Squashed 'third_party/allwpilib/' changes from f1a82828fe..ce550705d7

ce550705d7 [ntcore] Fix client "received unknown id -1" (#6186)
3989617bde [ntcore] NetworkTable::GetStruct: Add I template param (#6183)
f1836e1321 [fieldImages] Fix 2024 field json (#6179)
d05f179a9a [build] Fix running apriltagsvision Java example (#6173)
b1b03bed85 [wpilib] Update MotorControllerGroup deprecation message (#6171)
fa63fbf446 LICENSE.md: Bump year to 2024 (#6169)
4809f3d0fc [apriltag] Add 2024 AprilTag locations (#6168)
dd90965362 [wpiutil] Fix RawFrame.setInfo() NPE (#6167)
8659372d08 [fieldImages] Add 2024 field image (#6166)
a2e4d0b15d [docs] Fix docs for SysID routine (#6164)
0a46a3a618 [wpilib] Make ADXL345 default I2C address public (#6163)
7c26bc70ab [sysid] Load DataLog files directly for analysis (#6103)
f94e3d81b9 [docs] Fix SysId routine JavaDoc warnings (#6159)
6bed82a18e [wpilibc] Clean up C++ SysId routine (#6160)
4595f84719 [wpilib] Report LiveWindow-enabled-in-test (#6158)
707cb06105 [wpilib] Add SysIdRoutine logging utility and command factory (#6033)
3e40b9e5da [wpilib] Correct SmartDashboard usage reporting (#6157)
106518c3f8 [docs] Fix wpilibj JavaDoc warnings (#6154)
19cb2a8eb4 [wpilibj] Make class variables private to match C++ (#6153)
13f4460e00 [docs] Add missing docs to enum fields (NFC) (#6150)
4210f5635d [docs] Fix warnings about undocumented default constructors (#6151)
0f060afb55 [ntcore] Disable WebSocket fragmentation (#6149)
f29a7d2e50 [docs] Add missing JavaDocs (#6146)
6e58db398d [commands] Make Java fields private (#6148)
4ac0720385 [build] Clean up CMake files (#6141)
44db3e0ac0 [sysid] Make constexpr variables outside class scope inline (#6145)
73c7d87db7 [glass] NTStringChooser: Properly set retained (#6144)
25636b712f [build] Remove unnecessary native dependencies in wpilibjExamples (#6143)
01fb98baaa [docs] Add Missing JNI docs from C++ (NFC) (#6139)
5c424248c4 [wpilibj] Remove unused AnalogTriggerException (#6142)
c486972c55 [wpimath] Make ExponentialProfile.State mutable (#6138)
783acb9b72 [wpilibj] Store long preferences as integers (#6136)
99ab836894 [wpiutil] Add missing JavaDocs (NFC) (#6132)
ad0859a8c9 [docs] Add missing JavaDocs (#6125)
5579219716 [docs] Exclude quickbuf files and proto/struct packages from doclint (#6128)
98f06911c7 [sysid] Use eigenvector component instead of eigenvalue for fit quality check (#6131)
e1d49b975c [wpimath] Add LinearFilter reset() overload to initialize input and output buffers (#6133)
8a0bf2b7a4 [hal] Add CANAPITypes to java (#6121)
91d8837c11 [wpilib] Make protected fields in accelerometers/gyros private (#6134)
e7c9f27683 [wpilib] Add functional interface equivalents to MotorController (#6053)
8aca706217 [glass] Add type information to SmartDashboard menu (#6117)
7d3e4ddba9 [docs] Add warning about using user button to docs (NFC) (#6129)
ec3cb3dcba [build] Disable clang-tidy warning about test case names (#6127)
495585b25d [examples] Update april tag family to 36h11 (#6126)
f9aabc5ab2 [wpilib] Throw early when EventLoop is modified while running (#6115)
c16946c0ec [hal] Add CANJNI docs (NFC) (#6120)
b7f4eb2811 [doc] Update maven artifacts for units and apriltags (NFC) (#6123)
f419a62b38 [doc] Update maintainers.md (NFC) (#6124)
938bf45fd9 [wpiutil] Remove type param from ProtobufSerializable and StructSerializable (#6122)
c34debe012 [docs] Link to external OpenCV docs (#6119)
07183765de [hal] Fix formatting of HAL_ENUM enums (NFC) (#6114)
af46034b7f [wpilib] Document only first party controllers are guaranteed to have correct mapping (#6112)
636ef58d94 [hal] Properly error check readCANStreamSession (#6108)
cc631d2a69 [build] Fix generated source set location in the HAL (#6113)
09f76b32c2 [wpimath] Compile with UTF-8 encoding (#6111)
47c5fd8620 [sysid] Check data quality before OLS (#6110)
24a76be694 [hal] Add method to detect if the CAN Stream has overflowed (#6105)
9333951736 [hal] Allocate CANStreamMessage in JNI if null (#6107)
6a2d3c30a6 [wpiutil] Struct: Add info template parameter pack (#6086)
e07de37e64 [commands] Mark ParallelDeadlineGroup.setDeadline() final (#6102)
141241d2d6 [wpilib] Fix usage reporting for static classes (#6090)
f2c2bab7dc [sysid] Fix adjusted R² calculation (#6101)
5659038443 [wpiutil,cscore,apriltag] Fix RawFrame (#6098)
8aeee03626 [commands] Improve error message when composing commands twice in same composition (#6091)
55508706ff [wpiutil,cscore] Move VideoMode.PixelFormat to wpiutil (#6097)
ab78b930e9 [wpilib] ADIS16470: Add access to all 3 axes (#6074)
795d4be9fd [wpilib] Fix precision issue in Color round-and-clamp (#6100)
7aa9ad44b8 [commands] Deprecate C++ TransferOwnership() (#6095)
92c81d0791 [ci] Update pregenerate workflow to actions/checkout@v4 (#6094)
1ce617be07 [ci] Update artifact actions to v4 (#6092)
2441b57156 [wpilib] Add PWMSparkFlex MotorController (#6089)
21d1972d7a [wpiutil] DataLog: Ensure file is written on shutdown (#6087)
c29e8c66cf [wpiutil] DataLog: Fix UB in AppendImpl (#6088)
ab309e34ef [glass] Fix order of loading window settings (#6056)
22a322c9f3 [wpimath] Report error on negative PID gains (#6055)
1dba26c937 [wpilib] Add method to get breaker fault at a specific channel in PowerDistribution[Sticky]Faults (#5521)
ef1cb3f41e [commands] Fix compose-while-scheduled issue and test all compositions (#5581)
aeb1a4aa33 [wpiutil] Add serializable marker interfaces (#6060)
c1178d5add [wpilib] Add StadiaController and command wrapper (#6083)
4e4a468d4d [wpimath] Make feedforward classes throw exceptions for negative Kv or Ka (#6084)
d1793f077d [build] cmake: Add NO_WERROR option to disable -Werror (#6071)
43fb6e9f87 [glass] Add Profiled PID controller support & IZone Support (#5959)
bcef6c5398 [apriltag] Fix Java generation functions (#6063)
4059e0cd9f [hal,wpilib] Add function to control "Radio" LED (#6073)
0b2cfb3abc [dlt] Change datalogtool default folder to logs folder (#6079)
df5e439b0c [wpilib] PS4Controller: enable usage reporting (#6081)
0ff7478968 [cscore] Fix RawFrame class not being loaded in JNI (#6077)
6f23d32fe1 [wpilib] AddressableLED: Update warning about single driver (NFC) (#6069)
35a1c52788 [build] Upgrade quickbuf to 1.3.3 (#6072)
e4e2bafdb1 [sysid] Document timestamp units (#6065)
3d201c71f7 [ntcore] Fix overlapping subscriber handling (#6067)
f02984159f [glass] Check for null entries when updating struct/proto (#6059)
a004c9e05f [commands] SubsystemBase: allow setting name in constructor (#6052)
0b4c6a1546 [wpimath] Add more docs to SimulatedAnnealing (NFC) (#6054)
ab15dae887 [wpilib] ArcadeDrive: Fix max output handling (#6051)
9599c1f56f [hal] Add usage reporting ids from 2024v2 image (#6041)
f87c64af8a [wpimath] MecanumDriveWheelSpeeds: Fix desaturate() (#6040)
8798700cec [wpilibcExamples] Add inline specifier to constexpr constants (#6049)
85c9ae6eff [wpilib] Fix PS5 Controller mappings (#6050)
7c8b7a97ad [wpiutil] Zero out roborio system timestamp (#6042)
d9b504bc84 [wpilib] DataLogManager: Change sim location to logs subdir (#6039)
906b810136 [build] cmake: Fix ntcore generated header install (#6038)
56e5b404d1 Update to final 2024 V2 image (#6034)
8723ee5c39 [ntcore] Add cached topic property (#5494)
192a28af47 Fix JDK 21 warnings (#6028)
d40bdd70ba [build] Upgrade to spotbugs Gradle plugin 6.0.2 (#6027)
7bfadf32e5 [wpilibj] Joystick: make remainder of get axis methods final (#6024)
a770110438 [commands] CommandCompositionError: Include stacktrace of original composition (#5984)
54a55b8b53 [wpiutil,hal] Update image; init Rio Now() HMB with a FPGA session (#6016)
7d4e515a6b [wpimath] Simplify calculation of C for DARE precondition (#6022)
5200316c14 [ntcore] Update transmit period on topic add/remove (#6021)
ddf79a25d4 [wpiunits] Overload Measure.per(Time) to return Measure<Velocity> (#6018)
a71adef316 [wpiutil] Clean up circular_buffer iterator syntax (#6020)
39a0bf4b98 [examples] Call resetOdometry() when controller command is executed (#5905)
f5fc101fda [build] cmake: Export jars and clean up jar installs (#6014)
38bf024c96 [build] Update to Gradle 8.5 (#6007)
9d11544c18 [wpimath] Rotate traveling salesman solution so input and solution have same initial pose (#6015)
28deba20f5 [wpimath] Commit generated quickbuf Java files (#5994)
c2971c0bb3 [build] cmake: Export apriltag and wpimath (#6012)
41cfc961e4 gitattributes: Add linguist-generated locations (#6004)
14c3ade155 [wpimath] Struct cleanup (#6011)
90757b9e90 [wpilib] Make Color::HexString() constexpr (#5985)
2676b77873 Fix compilation issues that occur when building with bazel (#6008)
d32c10487c [examples] Update C++ examples to use CommandPtr (#5988)
9bc5fcf886 [build] cmake: Default WITH_JAVA_SOURCE to WITH_JAVA (#6005)
d431abba3b [upstream_utils] Fix GCEM namespace usage and add hypot(x, y, z) (#6002)
2bb1409b82 Clean up Java style (#5990)
66172ab288 Remove submodule (#6003)
e8f8c0ceb0 [upstream_utils] Update to latest Eigen HEAD (#5996)
890992a849 [hal] Commit generated usage reporting files (#5993)
a583ca01e1 [wpiutil] Change Struct to allow non-constexpr implementation (#5992)
ca272de400 [build] Fix Gradle compile_commands.json and clang-tidy warnings (#5977)
76ae090570 [wpiutil] type_traits: Add is_constexpr() (#5997)
5172ab8fd0 [commands] C++ CommandPtr: Prevent null initialization (#5991)
96914143ba [build] Bump native-utils to fix compile_commands.json (#5989)
464e6121ef [ci] Report failed status to Azure on failed tests (#2654)
5dad46cd45 [wpimath] Commit generated files (#5986)
54ab65a63a [ntcore] Commit generated files (#5962)
7ed900ae3a [wpilib] Add hex string constructor to Color and Color8Bit (#5063)
74b85b76a9 [wpimath] Make gcem call std functions if not constant-evaluated (#5983)
30816111db [wpimath] Fix TimeInterpolatableBuffer crash (#5972)
5cc923de33 [wpilib] DataLogManager: Use logs subdirectory on USB drives (#5975)
1144115da0 [commands] Add GetName to Subsystem, use in Scheduler tracer epochs (#5836)
ac7d726ac3 [wpimath] Add simulated annealing (#5961)
e09be72ee0 [wpimath] Remove unused SimpleMatrixUtils class (#5979)
0f9ebe92d9 [wpimath] Add generic circular buffer class to Java (#5969)
9fa28eb07a [ci] Bump actions/checkout to v4 (#5736)
ca684ac207 [hal] Add capability to read power distribution data as a stream (#4983)
51eecef2bd [wpimath] Optimize 2nd derivative of quintic splines (#3292)
4fcf0b25a1 [build] Apply a formatter for CMake files (#5973)
9b8011aa67 [build] Pin wpiformat version (#5982)
e00a0e84c1 [build] cmake: fix protobuf dependency finding for certain distributions (#5981)
23dd591394 [upstream_utils] Remove libuv patch that adjusts whitespace (#5976)
b0719942f0 [wpiutil] Timestamp: Report errors on Rio HMB init failure (#5974)
7bc89c4322 [wpilib] Update getAlliance() docs (NFC) (#5971)
841ea682d1 [upstream_utils] Upgrade to LLVM 17.0.5 (#5970)
a74db52dae [cameraserver] Add getVideo() pixelFormat overload (#5966)
a7eb422662 [build] Update native utils for new compile commands files (#5968)
544b231d4d [sysid] Add missing cassert include (#5967)
31cd015970 [wpimath] Add SysId doc links to LinearSystemId in C++ (NFC) (#5960)
9280054eab Revert "[build] Export wpimath protobuf symbols (#5952)"
2aba97c610 Export pb files from wpimath
c80b2d2017 [build] Export wpimath protobuf symbols (#5952)
3c0652c18a [cscore] Replace CS_PixelFormat with WPI_PixelFormat (#5954)
95716eb0cb [wpiunits] Documentation improvements (#5932)
423fd75fa8 [wpilib] Default LiveWindowEnabledInTest to false (#5950)
dfdea9c992 [wpimath] Make KalmanFilter variant for asymmetric updates (#5951)
ca81ced409 [wpiutil] Move RawFrame to wpiutil; add generation of RawFrame for AprilTags (#5923)
437cc91af5 [cscore] CvSink: Allow specifying output PixelFormat (#5943)
25b7dca46b [build] Remove CMake flat install option (#5944)
bb05e20247 [wpimath] Add protobuf/struct for trivial types (#5935)
35744a036e [wpimath] Move struct/proto classes to separate files (#5918)
80d7ad58ea [build] Declare platform launcher dependency explicitly (#5909)
f8d983b154 [ntcore] Protobuf/Struct: Use atomic_bool instead of atomic_flag (#5946)
4a44210ee3 [ntcore] NetworkTableInstance: Suppress unused lambda capture warning (#5947)
bdc8620d55 [upstream_utils] Fix fmt compilation errors on Windows (#5948)
0ca1e9b5f9 [wpimath] Add basic wpiunits support (#5821)
cc30824409 [ntcore] Increase client meta-topic decoding limit (#5934)
b1fad062f7 [wpilib] Use RKDP in DifferentialDrivetrainSim (#5931)
ead9ae5a69 [build] Add generateProto dependency to test and dev (#5933)
cfbff32185 [wpiutil] timestamp: Fix startup race on Rio (#5930)
7d90d0bcc3 [wpimath] Clean up StateSpaceUtil (#5891)
7755e45aac [build] Add generated protobuf headers to C++ test include path (#5926)
3985c031da [ntcore] ProtobufSubscriber: Fix typos (#5928)
7a87fe4b60 [ntcore] ProtobufSubscriber: Make mutex and msg mutable (#5927)
09f3ed6a5f [commands] Add static Trigger factories for robot mode changes (#5902)
79dd795bc0 [wpimath] Clean up VecBuilder and MatBuilder (#5906)
e117274a67 [wpilib] Change default Rio log dir from /home/lvuser to /home/lvuser/logs (#5899)
a8b80ca256 [upstream_utils] Update to libuv 1.47.0 (#5889)
b3a9c3e96b [build] Bump macOS deployment target to 12 (#5890)
0f8129677b [build] Distribute wpimath protobuf headers (#5925)
d105f9e3e9 [wpiutil] ProtobufBuffer: Fix buffer reallocation (#5924)
c5f2f6a0fb [fieldImages] Fix typo in field images artifact name (#5922)
c1a57e422a [commands] Clean up make_vector.h (#5917)
78ebc6e9ec [wpimath] change G to gearing in LinearSystemId factories (#5834)
9ada181866 [hal] DriverStation.h: Add stddef.h include (#5897)
95fa5ec72f [wpilibc,ntcoreffi] DataLogManager: join on Stop() call (#5910)
b6f2d3cc14 [build] Remove usage of Version.parse (#5911)
cc2cbeb04c [examples] Replace gyro rotation with poseEstimator rotation (#5900)
fa6b171e1c [wpiutil] Suppress protobuf warning false positives on GCC 13 (#5907)
d504639bbe [apriltag] Improve AprilTag docs (#5895)
3a1194be40 Replace static_cast<void>() with [[maybe_unused]] attribute (#5892)
70392cbbcb [build] cmake: Add protobuf dependency to wpiutil-config (#5886)
17c1bd5a83 [ntcore] Use json_fwd (#5881)
e69a9efeba [wpilibcExamples] Match array parameter bounds (#5880)
14dcd0d26f Use char instead of uint8_t for json::parse (#5877)
ec1d261984 [hal] Fix garbage data for match info before DS connection (#5879)
63dbf5c614 [wpiutil] MemoryBuffer: Fix normal read and file type check (#5875)
b2e7be9250 [ntcore] Only datalog meta-topics if specifically requested (#5873)
201a42a3cd [wpimath] Reorder TrapezoidProfile.calculate() arguments (#5874)
04a781b4d7 [apriltag] Add GetTags to C++ version of AprilTagFieldLayout (#5872)
87a8a1ced4 [docs] Exclude eigen and protobuf from doxygen (#5871)

git-subtree-dir: third_party/allwpilib
git-subtree-split: ce550705d7cdab117c0153a202973fc026a81274
Signed-off-by: Maxwell Henderson <mxwhenderson@gmail.com>
Change-Id: Ic8645d0551d62b411b0a816c493f0f33291896a1
diff --git a/ntcore/.clang-tidy b/ntcore/.clang-tidy
new file mode 100644
index 0000000..41c15bb
--- /dev/null
+++ b/ntcore/.clang-tidy
@@ -0,0 +1,69 @@
+Checks:
+  'bugprone-assert-side-effect,
+  bugprone-bool-pointer-implicit-conversion,
+  bugprone-copy-constructor-init,
+  bugprone-dangling-handle,
+  bugprone-dynamic-static-initializers,
+  bugprone-forwarding-reference-overload,
+  bugprone-inaccurate-erase,
+  bugprone-incorrect-roundings,
+  bugprone-integer-division,
+  bugprone-lambda-function-name,
+  bugprone-misplaced-operator-in-strlen-in-alloc,
+  bugprone-misplaced-widening-cast,
+  bugprone-move-forwarding-reference,
+  bugprone-multiple-statement-macro,
+  bugprone-parent-virtual-call,
+  bugprone-posix-return,
+  bugprone-sizeof-container,
+  bugprone-sizeof-expression,
+  bugprone-spuriously-wake-up-functions,
+  bugprone-string-constructor,
+  bugprone-string-integer-assignment,
+  bugprone-string-literal-with-embedded-nul,
+  bugprone-suspicious-enum-usage,
+  bugprone-suspicious-include,
+  bugprone-suspicious-memset-usage,
+  bugprone-suspicious-missing-comma,
+  bugprone-suspicious-semicolon,
+  bugprone-suspicious-string-compare,
+  bugprone-throw-keyword-missing,
+  bugprone-too-small-loop-variable,
+  bugprone-undefined-memory-manipulation,
+  bugprone-undelegated-constructor,
+  bugprone-unhandled-self-assignment,
+  bugprone-unused-raii,
+  bugprone-virtual-near-miss,
+  cert-err52-cpp,
+  cert-err60-cpp,
+  cert-mem57-cpp,
+  cert-oop57-cpp,
+  cert-oop58-cpp,
+  clang-diagnostic-*,
+  -clang-diagnostic-deprecated-declarations,
+  -clang-diagnostic-#warnings,
+  -clang-diagnostic-pedantic,
+  clang-analyzer-*,
+  cppcoreguidelines-slicing,
+  google-build-namespaces,
+  google-explicit-constructor,
+  google-global-names-in-headers,
+  google-readability-avoid-underscore-in-googletest-name,
+  google-readability-casting,
+  google-runtime-operator,
+  misc-definitions-in-headers,
+  misc-misplaced-const,
+  misc-new-delete-overloads,
+  misc-non-copyable-objects,
+  modernize-avoid-bind,
+  modernize-concat-nested-namespaces,
+  modernize-make-shared,
+  modernize-make-unique,
+  modernize-pass-by-value,
+  modernize-use-default-member-init,
+  modernize-use-noexcept,
+  modernize-use-nullptr,
+  modernize-use-override,
+  modernize-use-using,
+  readability-braces-around-statements'
+FormatStyle: file
diff --git a/ntcore/.styleguide b/ntcore/.styleguide
index 1808bb5..13634e4 100644
--- a/ntcore/.styleguide
+++ b/ntcore/.styleguide
@@ -14,6 +14,8 @@
 
 generatedFileExclude {
   ntcore/doc/
+  ntcore/src/generated
+  .*\.jinja
 }
 
 repoRootNameOverride {
diff --git a/ntcore/CMakeLists.txt b/ntcore/CMakeLists.txt
index 5216eda..2b9d326 100644
--- a/ntcore/CMakeLists.txt
+++ b/ntcore/CMakeLists.txt
@@ -3,30 +3,25 @@
 include(CompileWarnings)
 include(AddTest)
 
-execute_process(COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/generate_topics.py ${WPILIB_BINARY_DIR}/ntcore RESULT_VARIABLE generateResult)
-if(NOT (generateResult EQUAL "0"))
-  # Try python
-  execute_process(COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/generate_topics.py ${WPILIB_BINARY_DIR}/ntcore RESULT_VARIABLE generateResult)
-  if(NOT (generateResult EQUAL "0"))
-    message(FATAL_ERROR "python and python3 generate_topics.py failed")
-  endif()
-endif()
-
-file(GLOB ntcore_native_src
+file(
+    GLOB ntcore_native_src
     src/main/native/cpp/*.cpp
-    ${WPILIB_BINARY_DIR}/ntcore/generated/main/native/cpp/*.cpp
+    src/generated/main/native/cpp/*.cpp
     src/main/native/cpp/net/*.cpp
     src/main/native/cpp/net3/*.cpp
     src/main/native/cpp/networktables/*.cpp
-    src/main/native/cpp/tables/*.cpp)
+    src/main/native/cpp/tables/*.cpp
+)
 add_library(ntcore ${ntcore_native_src})
 set_target_properties(ntcore PROPERTIES DEBUG_POSTFIX "d")
-target_include_directories(ntcore
-                PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/cpp
-                PUBLIC
-                $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
-                $<BUILD_INTERFACE:${WPILIB_BINARY_DIR}/ntcore/generated/main/native/include>
-                            $<INSTALL_INTERFACE:${include_dest}/ntcore>)
+target_include_directories(
+    ntcore
+    PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/cpp
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/generated/main/native/include>
+        $<INSTALL_INTERFACE:${include_dest}/ntcore>
+)
 wpilib_target_warnings(ntcore)
 target_compile_features(ntcore PUBLIC cxx_std_20)
 target_link_libraries(ntcore PUBLIC wpinet wpiutil)
@@ -35,43 +30,38 @@
 
 install(TARGETS ntcore EXPORT ntcore)
 install(DIRECTORY src/main/native/include/ DESTINATION "${include_dest}/ntcore")
-install(DIRECTORY ${WPILIB_BINARY_DIR}/ntcore/generated/main/native/include/ DESTINATION "${include_dest}/ntcore")
+install(DIRECTORY src/generated/main/native/include/ DESTINATION "${include_dest}/ntcore")
 
-if (WITH_FLAT_INSTALL)
-    set (ntcore_config_dir ${wpilib_dest})
-else()
-    set (ntcore_config_dir share/ntcore)
-endif()
-
-configure_file(ntcore-config.cmake.in ${WPILIB_BINARY_DIR}/ntcore-config.cmake )
-install(FILES ${WPILIB_BINARY_DIR}/ntcore-config.cmake DESTINATION ${ntcore_config_dir})
-install(EXPORT ntcore DESTINATION ${ntcore_config_dir})
+configure_file(ntcore-config.cmake.in ${WPILIB_BINARY_DIR}/ntcore-config.cmake)
+install(FILES ${WPILIB_BINARY_DIR}/ntcore-config.cmake DESTINATION share/ntcore)
+install(EXPORT ntcore DESTINATION share/ntcore)
 
 # Java bindings
-if (WITH_JAVA)
+if(WITH_JAVA)
     find_package(Java REQUIRED)
     find_package(JNI REQUIRED)
     include(UseJava)
     set(CMAKE_JAVA_COMPILE_FLAGS "-encoding" "UTF8" "-Xlint:unchecked")
 
-    file(GLOB QUICKBUF_JAR
-        ${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf/*.jar)
+    file(GLOB QUICKBUF_JAR ${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf/*.jar)
 
     set(CMAKE_JAVA_INCLUDE_PATH wpimath.jar ${QUICKBUF_JAR})
 
-    file(GLOB ntcore_jni_src
-        src/main/native/cpp/jni/*.cpp
-        ${WPILIB_BINARY_DIR}/ntcore/generated/main/native/cpp/jni/*.cpp)
+    file(GLOB ntcore_jni_src src/main/native/cpp/jni/*.cpp src/generated/main/native/cpp/jni/*.cpp)
 
-    file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java ${WPILIB_BINARY_DIR}/ntcore/generated/*.java)
+    file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java src/generated/main/java/*.java)
     set(CMAKE_JNI_TARGET true)
 
-    add_jar(ntcore_jar ${JAVA_SOURCES} INCLUDE_JARS wpiutil_jar OUTPUT_NAME ntcore GENERATE_NATIVE_HEADERS ntcore_jni_headers)
+    add_jar(
+        ntcore_jar
+        ${JAVA_SOURCES}
+        INCLUDE_JARS wpiutil_jar
+        OUTPUT_NAME ntcore
+        GENERATE_NATIVE_HEADERS ntcore_jni_headers
+    )
 
-    get_property(NTCORE_JAR_FILE TARGET ntcore_jar PROPERTY JAR_FILE)
-    install(FILES ${NTCORE_JAR_FILE} DESTINATION "${java_lib_dest}")
-
-    set_property(TARGET ntcore_jar PROPERTY FOLDER "java")
+    install_jar(ntcore_jar DESTINATION ${java_lib_dest})
+    install_jar_exports(TARGETS ntcore_jar FILE ntcore_jar.cmake DESTINATION share/ntcore)
 
     add_library(ntcorejni ${ntcore_jni_src})
     wpilib_target_warnings(ntcorejni)
@@ -79,7 +69,7 @@
 
     set_property(TARGET ntcorejni PROPERTY FOLDER "libraries")
 
-    if (MSVC)
+    if(MSVC)
         install(TARGETS ntcorejni RUNTIME DESTINATION "${jni_lib_dest}" COMPONENT Runtime)
     endif()
 
@@ -87,16 +77,22 @@
     add_dependencies(ntcorejni ntcore_jar)
 
     install(TARGETS ntcorejni EXPORT ntcorejni)
-
 endif()
 
-if (WITH_JAVA_SOURCE)
+if(WITH_JAVA_SOURCE)
     find_package(Java REQUIRED)
     include(UseJava)
-    file(GLOB NTCORE_SOURCES src/main/java/edu/wpi/first/networktables/*.java ${WPILIB_BINARY_DIR}/ntcore/generated/*.java)
-    add_jar(ntcore_src_jar
-    RESOURCES NAMESPACE "edu/wpi/first/networktables" ${NTCORE_SOURCES}
-    OUTPUT_NAME ntcore-sources)
+    file(
+        GLOB NTCORE_SOURCES
+        src/main/java/edu/wpi/first/networktables/*.java
+        src/generated/main/java/*.java
+    )
+    add_jar(
+        ntcore_src_jar
+        RESOURCES
+        NAMESPACE "edu/wpi/first/networktables" ${NTCORE_SOURCES}
+        OUTPUT_NAME ntcore-sources
+    )
 
     get_property(NTCORE_SRC_JAR_FILE TARGET ntcore_src_jar PROPERTY JAR_FILE)
     install(FILES ${NTCORE_SRC_JAR_FILE} DESTINATION "${java_lib_dest}")
@@ -108,7 +104,7 @@
 wpilib_target_warnings(ntcoredev)
 target_link_libraries(ntcoredev ntcore)
 
-if (WITH_TESTS)
+if(WITH_TESTS)
     wpilib_add_test(ntcore src/test/native/cpp)
     target_include_directories(ntcore_test PRIVATE src/main/native/cpp)
     target_link_libraries(ntcore_test ntcore gmock_main wpiutil_testlib)
diff --git a/ntcore/build.gradle b/ntcore/build.gradle
index c5464cc..510e5c1 100644
--- a/ntcore/build.gradle
+++ b/ntcore/build.gradle
@@ -1,290 +1,24 @@
-import groovy.json.JsonSlurper;
-import com.hubspot.jinjava.Jinjava;
-import com.hubspot.jinjava.JinjavaConfig;
-
-def ntcoreTypesInputFile = file("src/generate/types.json")
-def ntcoreJavaTypesInputDir = file("src/generate/java")
-def ntcoreJavaTypesOutputDir = file("$buildDir/generated/main/java/edu/wpi/first/networktables")
-
-task ntcoreGenerateJavaTypes() {
-    description = "Generates ntcore Java type classes"
-    group = "WPILib"
-
-    inputs.file ntcoreTypesInputFile
-    inputs.dir ntcoreJavaTypesInputDir
-    outputs.dir ntcoreJavaTypesOutputDir
-
-    doLast {
-        def jsonSlurper = new JsonSlurper()
-        def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
-
-        ntcoreJavaTypesOutputDir.deleteDir()
-        ntcoreJavaTypesOutputDir.mkdirs()
-
-        def config = new JinjavaConfig()
-        def jinjava = new Jinjava(config)
-
-        ntcoreJavaTypesInputDir.listFiles().each { File file ->
-            def template = file.text
-            def outfn = file.name.substring(0, file.name.length() - 6)
-            if (file.name.startsWith("NetworkTable") || file.name.startsWith("Generic")) {
-                def replacements = new HashMap<String,?>()
-                replacements.put("types", jsonTypes)
-                def output = jinjava.render(template, replacements)
-                new File(ntcoreJavaTypesOutputDir, outfn).write(output)
-            } else {
-                jsonTypes.each { Map<String,?> replacements ->
-                    def output = jinjava.render(template, replacements)
-                    def typename = replacements.get("TypeName")
-                    File outfile
-                    if (outfn == "Timestamped.java") {
-                        outfile = new File(ntcoreJavaTypesOutputDir, "Timestamped${typename}.java")
-                    } else {
-                        outfile = new File(ntcoreJavaTypesOutputDir, "${typename}${outfn}")
-                    }
-                    outfile.write(output)
-                }
-            }
-        }
-    }
-}
-
-def ntcoreCppTypesInputDir = file("src/generate/include/networktables")
-def ntcoreCppTypesOutputDir = file("$buildDir/generated/main/native/include/networktables")
-
-task ntcoreGenerateCppTypes() {
-    description = "Generates ntcore C++ type classes"
-    group = "WPILib"
-
-    inputs.file ntcoreTypesInputFile
-    inputs.dir ntcoreCppTypesInputDir
-    outputs.dir ntcoreCppTypesOutputDir
-
-    doLast {
-        def jsonSlurper = new JsonSlurper()
-        def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
-
-        ntcoreCppTypesOutputDir.deleteDir()
-        ntcoreCppTypesOutputDir.mkdirs()
-
-        def config = new JinjavaConfig()
-        def jinjava = new Jinjava(config)
-
-        ntcoreCppTypesInputDir.listFiles().each { File file ->
-            def template = file.text
-            def outfn = file.name.substring(0, file.name.length() - 6)
-            jsonTypes.each { Map<String,?> replacements ->
-                def output = jinjava.render(template, replacements)
-                def typename = replacements.get("TypeName")
-                def outfile = new File(ntcoreCppTypesOutputDir, "${typename}${outfn}")
-                outfile.write(output)
-            }
-        }
-    }
-}
-
-def ntcoreCppHandleSourceInputFile = file("src/generate/cpp/ntcore_cpp_types.cpp.jinja")
-def ntcoreCppHandleSourceOutputFile = file("$buildDir/generated/main/native/cpp/ntcore_cpp_types.cpp")
-
-task ntcoreGenerateCppHandleSource() {
-    description = "Generates ntcore C++ handle source"
-    group = "WPILib"
-
-    inputs.files([
-        ntcoreTypesInputFile,
-        ntcoreCppHandleSourceInputFile
-    ])
-    outputs.file ntcoreCppHandleSourceOutputFile
-
-    doLast {
-        def jsonSlurper = new JsonSlurper()
-        def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
-
-        ntcoreCppHandleSourceOutputFile.delete()
-
-        def config = new JinjavaConfig()
-        def jinjava = new Jinjava(config)
-
-        def template = ntcoreCppHandleSourceInputFile.text
-        def replacements = new HashMap<String,?>()
-        replacements.put("types", jsonTypes)
-        def output = jinjava.render(template, replacements)
-        ntcoreCppHandleSourceOutputFile.write(output)
-    }
-}
-
-def ntcoreCppHandleHeaderInputFile = file("src/generate/include/ntcore_cpp_types.h.jinja")
-def ntcoreCppHandleHeaderOutputFile = file("$buildDir/generated/main/native/include/ntcore_cpp_types.h")
-
-task ntcoreGenerateCppHandleHeader() {
-    description = "Generates ntcore C++ handle header"
-    group = "WPILib"
-
-    inputs.files([
-        ntcoreTypesInputFile,
-        ntcoreCppHandleHeaderInputFile
-    ])
-    outputs.file ntcoreCppHandleHeaderOutputFile
-
-    doLast {
-        def jsonSlurper = new JsonSlurper()
-        def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
-
-        ntcoreCppHandleHeaderOutputFile.delete()
-
-        def config = new JinjavaConfig()
-        def jinjava = new Jinjava(config)
-
-        def template = ntcoreCppHandleHeaderInputFile.text
-        def replacements = new HashMap<String,?>()
-        replacements.put("types", jsonTypes)
-        def output = jinjava.render(template, replacements)
-        ntcoreCppHandleHeaderOutputFile.write(output)
-    }
-}
-
-def ntcoreCHandleSourceInputFile = file("src/generate/cpp/ntcore_c_types.cpp.jinja")
-def ntcoreCHandleSourceOutputFile = file("$buildDir/generated/main/native/cpp/ntcore_c_types.cpp")
-
-task ntcoreGenerateCHandleSource() {
-    description = "Generates ntcore C handle source"
-    group = "WPILib"
-
-    inputs.files([
-        ntcoreTypesInputFile,
-        ntcoreCHandleSourceInputFile
-    ])
-    outputs.file ntcoreCHandleSourceOutputFile
-
-    doLast {
-        def jsonSlurper = new JsonSlurper()
-        def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
-
-        ntcoreCHandleSourceOutputFile.delete()
-
-        def config = new JinjavaConfig()
-        def jinjava = new Jinjava(config)
-
-        def template = ntcoreCHandleSourceInputFile.text
-        def replacements = new HashMap<String,?>()
-        replacements.put("types", jsonTypes)
-        def output = jinjava.render(template, replacements)
-        ntcoreCHandleSourceOutputFile.write(output)
-    }
-}
-
-def ntcoreCHandleHeaderInputFile = file("src/generate/include/ntcore_c_types.h.jinja")
-def ntcoreCHandleHeaderOutputFile = file("$buildDir/generated/main/native/include/ntcore_c_types.h")
-
-task ntcoreGenerateCHandleHeader() {
-    description = "Generates ntcore C handle header"
-    group = "WPILib"
-
-    inputs.files([
-        ntcoreTypesInputFile,
-        ntcoreCHandleHeaderInputFile
-    ])
-    outputs.file ntcoreCHandleHeaderOutputFile
-
-    doLast {
-        def jsonSlurper = new JsonSlurper()
-        def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
-
-        ntcoreCHandleHeaderOutputFile.delete()
-
-        def config = new JinjavaConfig()
-        def jinjava = new Jinjava(config)
-
-        def template = ntcoreCHandleHeaderInputFile.text
-        def replacements = new HashMap<String,?>()
-        replacements.put("types", jsonTypes)
-        def output = jinjava.render(template, replacements)
-        ntcoreCHandleHeaderOutputFile.write(output)
-    }
-}
-
-def ntcoreJniSourceInputFile = file("src/generate/cpp/jni/types_jni.cpp.jinja")
-def ntcoreJniSourceOutputFile = file("$buildDir/generated/main/native/cpp/jni/types_jni.cpp")
-
-task ntcoreGenerateJniSource() {
-    description = "Generates ntcore JNI types source"
-    group = "WPILib"
-
-    inputs.files([
-        ntcoreTypesInputFile,
-        ntcoreJniSourceInputFile
-    ])
-    outputs.file ntcoreJniSourceOutputFile
-
-    doLast {
-        def jsonSlurper = new JsonSlurper()
-        def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
-
-        ntcoreJniSourceOutputFile.delete()
-
-        def config = new JinjavaConfig()
-        def jinjava = new Jinjava(config)
-
-        def template = ntcoreJniSourceInputFile.text
-        def replacements = new HashMap<String,?>()
-        replacements.put("types", jsonTypes)
-        def output = jinjava.render(template, replacements)
-        ntcoreJniSourceOutputFile.write(output)
-    }
-}
-
 ext {
     addNtcoreDependency = { binary, shared->
-        binary.tasks.withType(AbstractNativeSourceCompileTask) {
-            it.dependsOn ntcoreGenerateCppTypes
-            it.dependsOn ntcoreGenerateCppHandleHeader
-            it.dependsOn ntcoreGenerateCHandleHeader
-        }
         binary.lib project: ':ntcore', library: 'ntcore', linkage: shared
     }
 
     addNtcoreJniDependency = { binary->
-        binary.tasks.withType(AbstractNativeSourceCompileTask) {
-            it.dependsOn ntcoreGenerateCppTypes
-            it.dependsOn ntcoreGenerateCppHandleHeader
-            it.dependsOn ntcoreGenerateCHandleHeader
-        }
         binary.lib project: ':ntcore', library: 'ntcoreJNIShared', linkage: 'shared'
     }
 
     nativeName = 'ntcore'
     devMain = 'edu.wpi.first.ntcore.DevMain'
-    generatedSources = "$buildDir/generated/main/native/cpp"
-    generatedHeaders = "$buildDir/generated/main/native/include"
+    generatedSources = "$projectDir/src/generated/main/native/cpp"
+    generatedHeaders = "$projectDir/src/generated/main/native/include"
     jniSplitSetup = {
-        it.tasks.withType(CppCompile) {
-            it.dependsOn ntcoreGenerateCppTypes
-            it.dependsOn ntcoreGenerateCppHandleSource
-            it.dependsOn ntcoreGenerateCppHandleHeader
-            it.dependsOn ntcoreGenerateCHandleSource
-            it.dependsOn ntcoreGenerateCHandleHeader
-            it.dependsOn ntcoreGenerateJniSource
-        }
     }
     splitSetup = {
         it.tasks.withType(CppCompile) {
-            it.dependsOn ntcoreGenerateCppTypes
-            it.dependsOn ntcoreGenerateCppHandleSource
-            it.dependsOn ntcoreGenerateCppHandleHeader
-            it.dependsOn ntcoreGenerateCHandleSource
-            it.dependsOn ntcoreGenerateCHandleHeader
-            it.dependsOn ntcoreGenerateJniSource
             it.includes 'src/main/native/cpp'
         }
     }
     exeSplitSetup = {
-        it.tasks.withType(CppCompile) {
-            it.dependsOn ntcoreGenerateCppTypes
-            it.dependsOn ntcoreGenerateCppHandleSource
-            it.dependsOn ntcoreGenerateCppHandleHeader
-            it.dependsOn ntcoreGenerateCHandleSource
-            it.dependsOn ntcoreGenerateCHandleHeader
-        }
     }
 }
 
@@ -308,13 +42,9 @@
     }
 }
 
-sourceSets.main.java.srcDir "${buildDir}/generated/main/java"
-compileJava.dependsOn ntcoreGenerateJavaTypes
+sourceSets.main.java.srcDir "${projectDir}/src/generated/main/java"
 
 cppHeadersZip {
-    dependsOn ntcoreGenerateCppTypes
-    dependsOn ntcoreGenerateCppHandleHeader
-    dependsOn ntcoreGenerateCHandleHeader
     from(generatedHeaders) {
         into '/'
     }
diff --git a/ntcore/doc/networktables4.adoc b/ntcore/doc/networktables4.adoc
index da157e5..29e74a1 100644
--- a/ntcore/doc/networktables4.adoc
+++ b/ntcore/doc/networktables4.adoc
@@ -23,7 +23,7 @@
 * Recommend that timestamp synchronization occur immediately following connection establishment and prior to any other control messages
 * Recommend text and binary combining into a single WebSockets frame be limited to the network MTU (unless necessary to transport the message)
 * Recommend WebSockets fragmentation be used on large frames to enable rapid handling of PING messages
-* Add an option for topics to be marked transient (in which case no last value is retained by the server or sent to clients on initial subscription)
+* Add a (default true) option for topics to be marked cached (in which case the last value is retained by the server and sent to clients on initial subscription)
 * Recommend clients subscribe to the `$sub$<topic>` meta-topic for each topic published by the client, and use this information to control what value updates are sent over the network to the server
 
 Version 4.1 uses a different WebSockets subprotocol string than version 4.0, so it is easy for both clients and servers to simultaneously support both versions 4.0 and 4.1. Due to WebSockets implementation bugs in version 4.0, version 4.1 implementations must not send WebSockets PING messages on version 4.0 connections.
@@ -100,7 +100,7 @@
 [[reconnection]]
 === Caching and Reconnection Handling
 
-Servers shall keep a retained value for each topic for the purposes of <<msg-subscribe>> requests; the retained value shall be the value in the largest timestamp (greater-than or equal-to) message received for that topic.  This retained value is deleted if the topic is deleted (e.g. there are no more publishers).
+Servers shall keep a retained value for each cached topic for the purposes of <<msg-subscribe>> requests; the retained value shall be the value in the largest timestamp (greater-than or equal-to) message received for that topic.  This retained value is deleted if the topic is deleted (e.g. there are no more publishers).
 
 Clients may similarly keep a retained value for each topic for ease of use by user code.  If this is done, this retained value shall be updated by both locally published values and received messages for that topic with greater-than/equal-to timestamps, and the retained value shall be deleted when a <<msg-unannounce>> is received.
 
@@ -419,6 +419,7 @@
 |Property|Type|Description|Notes
 |`persistent`|boolean|Persistent Flag|If true, the last set value will be periodically saved to persistent storage on the server and be restored during server startup.  Topics with this property set to true will not be deleted by the server when the last publisher stops publishing.
 |`retained`|boolean|Retained Flag|Topics with this property set to true will not be deleted by the server when the last publisher stops publishing.
+|`cached`|boolean|Cached Flag|If false, the server and clients will not store the value of the topic.  This means that only value updates will be available for the topic.
 |===
 
 [[sub-options]]
@@ -619,7 +620,7 @@
 [[msg-subscribe]]
 ==== Subscribe Message (`subscribe`)
 
-Sent from a client to the server to indicate the client wants to subscribe to value changes for the specified topics / groups of topics.  The server shall send MessagePack messages containing the current values for any existing topics upon receipt, and continue sending MessagePack messages for future value changes.  If a topic does not yet exist, no message is sent until it is created (via a publish), at which point a <<msg-announce>> will be sent and MessagePack messages will automatically follow as they are published.
+Sent from a client to the server to indicate the client wants to subscribe to value changes for the specified topics / groups of topics.  The server shall send MessagePack messages containing the current values for any existing cached topics upon receipt, and continue sending MessagePack messages for future value changes.  If a topic does not yet exist, no message is sent until it is created (via a publish), at which point a <<msg-announce>> will be sent and MessagePack messages will automatically follow as they are published.
 
 Subscriptions may overlap; only one MessagePack message is sent per value change regardless of the number of subscriptions.  Sending a `subscribe` message with the same subscription UID as a previous `subscribe` message results in updating the subscription (replacing the array of identifiers and updating any specified options).
 
diff --git a/ntcore/generate_topics.py b/ntcore/generate_topics.py
old mode 100644
new mode 100755
index ece7df2..5afd61a
--- a/ntcore/generate_topics.py
+++ b/ntcore/generate_topics.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python3
+
 import glob
 import os
 import sys
@@ -17,23 +19,22 @@
                 return
 
     # File either doesn't exist or has different contents
-    with open(outpathname, "w") as f:
+    with open(outpathname, "w", newline="\n") as f:
         f.write(contents)
 
 
 def main():
     dirname, _ = os.path.split(os.path.abspath(__file__))
-    cmake_binary_dir = sys.argv[1]
 
     with open(f"{dirname}/src/generate/types.json") as f:
         types = json.load(f)
 
     # Java files
     env = Environment(
-        loader=FileSystemLoader(f"{dirname}/src/generate/java"), autoescape=False
+        loader=FileSystemLoader(f"{dirname}/src/generate/main/java"), autoescape=False
     )
-    rootPath = f"{cmake_binary_dir}/generated/main/java/edu/wpi/first/networktables"
-    for fn in glob.glob(f"{dirname}/src/generate/java/*.jinja"):
+    rootPath = f"{dirname}/src/generated/main/java/edu/wpi/first/networktables"
+    for fn in glob.glob(f"{dirname}/src/generate/main/java/*.jinja"):
         template = env.get_template(os.path.basename(fn))
         outfn = os.path.basename(fn)[:-6]  # drop ".jinja"
         if os.path.basename(fn).startswith("NetworkTable") or os.path.basename(
@@ -52,11 +53,15 @@
 
     # C++ classes
     env = Environment(
-        loader=FileSystemLoader(f"{dirname}/src/generate/include/networktables"),
+        loader=FileSystemLoader(
+            f"{dirname}/src/generate/main/native/include/networktables"
+        ),
         autoescape=False,
     )
-    rootPath = f"{cmake_binary_dir}/generated/main/native/include/networktables"
-    for fn in glob.glob(f"{dirname}/src/generate/include/networktables/*.jinja"):
+    rootPath = f"{dirname}/src/generated/main/native/include/networktables"
+    for fn in glob.glob(
+        f"{dirname}/src/generate/main/native/include/networktables/*.jinja"
+    ):
         template = env.get_template(os.path.basename(fn))
         outfn = os.path.basename(fn)[:-6]  # drop ".jinja"
         for replacements in types:
@@ -66,55 +71,56 @@
 
     # C++ handle API (header)
     env = Environment(
-        loader=FileSystemLoader(f"{dirname}/src/generate/include"), autoescape=False
+        loader=FileSystemLoader(f"{dirname}/src/generate/main/native/include"),
+        autoescape=False,
     )
     template = env.get_template("ntcore_cpp_types.h.jinja")
     output = template.render(types=types)
     Output(
-        f"{cmake_binary_dir}/generated/main/native/include",
+        f"{dirname}/src/generated/main/native/include",
         "ntcore_cpp_types.h",
         output,
     )
 
     # C++ handle API (source)
     env = Environment(
-        loader=FileSystemLoader(f"{dirname}/src/generate/cpp"), autoescape=False
+        loader=FileSystemLoader(f"{dirname}/src/generate/main/native/cpp"),
+        autoescape=False,
     )
     template = env.get_template("ntcore_cpp_types.cpp.jinja")
     output = template.render(types=types)
-    Output(
-        f"{cmake_binary_dir}/generated/main/native/cpp", "ntcore_cpp_types.cpp", output
-    )
+    Output(f"{dirname}/src/generated/main/native/cpp", "ntcore_cpp_types.cpp", output)
 
     # C handle API (header)
     env = Environment(
-        loader=FileSystemLoader(f"{dirname}/src/generate/include"), autoescape=False
+        loader=FileSystemLoader(f"{dirname}/src/generate/main/native/include"),
+        autoescape=False,
     )
     template = env.get_template("ntcore_c_types.h.jinja")
     output = template.render(types=types)
     Output(
-        f"{cmake_binary_dir}/generated/main/native/include",
+        f"{dirname}/src/generated/main/native/include",
         "ntcore_c_types.h",
         output,
     )
 
     # C handle API (source)
     env = Environment(
-        loader=FileSystemLoader(f"{dirname}/src/generate/cpp"), autoescape=False
+        loader=FileSystemLoader(f"{dirname}/src/generate/main/native/cpp"),
+        autoescape=False,
     )
     template = env.get_template("ntcore_c_types.cpp.jinja")
     output = template.render(types=types)
-    Output(
-        f"{cmake_binary_dir}/generated/main/native/cpp", "ntcore_c_types.cpp", output
-    )
+    Output(f"{dirname}/src/generated/main/native/cpp", "ntcore_c_types.cpp", output)
 
     # JNI
     env = Environment(
-        loader=FileSystemLoader(f"{dirname}/src/generate/cpp/jni"), autoescape=False
+        loader=FileSystemLoader(f"{dirname}/src/generate/main/native/cpp/jni"),
+        autoescape=False,
     )
     template = env.get_template("types_jni.cpp.jinja")
     output = template.render(types=types)
-    Output(f"{cmake_binary_dir}/generated/main/native/cpp/jni", "types_jni.cpp", output)
+    Output(f"{dirname}/src/generated/main/native/cpp/jni", "types_jni.cpp", output)
 
 
 if __name__ == "__main__":
diff --git a/ntcore/ntcore-config.cmake.in b/ntcore/ntcore-config.cmake.in
index 0a85f8b..9642d63 100644
--- a/ntcore/ntcore-config.cmake.in
+++ b/ntcore/ntcore-config.cmake.in
@@ -5,3 +5,6 @@
 
 @FILENAME_DEP_REPLACE@
 include(${SELF_DIR}/ntcore.cmake)
+if(@WITH_JAVA@)
+    include(${SELF_DIR}/ntcore_jar.cmake)
+endif()
diff --git a/ntcore/src/dev/native/cpp/main.cpp b/ntcore/src/dev/native/cpp/main.cpp
index 6e43fdb..d724bbe 100644
--- a/ntcore/src/dev/native/cpp/main.cpp
+++ b/ntcore/src/dev/native/cpp/main.cpp
@@ -24,7 +24,7 @@
 void stress();
 
 int main(int argc, char* argv[]) {
-  wpi::impl::SetupNowRio();
+  wpi::impl::SetupNowDefaultOnRio();
 
   if (argc == 2 && std::string_view{argv[1]} == "bench") {
     bench();
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generate/main/java/Entry.java.jinja
similarity index 86%
rename from ntcore/src/generate/java/Entry.java.jinja
rename to ntcore/src/generate/main/java/Entry.java.jinja
index cbaa782..43c424f 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generate/main/java/Entry.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
@@ -13,3 +15,4 @@
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
+
diff --git a/ntcore/src/generate/java/EntryImpl.java.jinja b/ntcore/src/generate/main/java/EntryImpl.java.jinja
similarity index 97%
rename from ntcore/src/generate/java/EntryImpl.java.jinja
rename to ntcore/src/generate/main/java/EntryImpl.java.jinja
index b7432a7..5053e10 100644
--- a/ntcore/src/generate/java/EntryImpl.java.jinja
+++ b/ntcore/src/generate/main/java/EntryImpl.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 {% if TypeName == "Raw" %}
 import java.nio.ByteBuffer;
@@ -95,3 +97,4 @@
   private final {{ TypeName }}Topic m_topic;
   private final {{ java.ValueType }} m_defaultValue;
 }
+
diff --git a/ntcore/src/generate/java/GenericEntryImpl.java.jinja b/ntcore/src/generate/main/java/GenericEntryImpl.java.jinja
similarity index 99%
rename from ntcore/src/generate/java/GenericEntryImpl.java.jinja
rename to ntcore/src/generate/main/java/GenericEntryImpl.java.jinja
index 29666bb..0bc4fed 100644
--- a/ntcore/src/generate/java/GenericEntryImpl.java.jinja
+++ b/ntcore/src/generate/main/java/GenericEntryImpl.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import java.nio.ByteBuffer;
@@ -373,3 +375,4 @@
 
   private final Topic m_topic;
 }
+
diff --git a/ntcore/src/generate/java/GenericPublisher.java.jinja b/ntcore/src/generate/main/java/GenericPublisher.java.jinja
similarity index 98%
rename from ntcore/src/generate/java/GenericPublisher.java.jinja
rename to ntcore/src/generate/main/java/GenericPublisher.java.jinja
index 881aba6..f6d7001 100644
--- a/ntcore/src/generate/java/GenericPublisher.java.jinja
+++ b/ntcore/src/generate/main/java/GenericPublisher.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import java.nio.ByteBuffer;
@@ -242,3 +244,4 @@
     set(value);
   }
 }
+
diff --git a/ntcore/src/generate/java/GenericSubscriber.java.jinja b/ntcore/src/generate/main/java/GenericSubscriber.java.jinja
similarity index 95%
rename from ntcore/src/generate/java/GenericSubscriber.java.jinja
rename to ntcore/src/generate/main/java/GenericSubscriber.java.jinja
index 63ecebc..c0ed661 100644
--- a/ntcore/src/generate/java/GenericSubscriber.java.jinja
+++ b/ntcore/src/generate/main/java/GenericSubscriber.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import java.util.function.Supplier;
@@ -56,3 +58,4 @@
    */
   NetworkTableValue[] readQueue();
 }
+
diff --git a/ntcore/src/generate/java/NetworkTableEntry.java.jinja b/ntcore/src/generate/main/java/NetworkTableEntry.java.jinja
similarity index 99%
rename from ntcore/src/generate/java/NetworkTableEntry.java.jinja
rename to ntcore/src/generate/main/java/NetworkTableEntry.java.jinja
index 7ee7d1f..2c2c737 100644
--- a/ntcore/src/generate/java/NetworkTableEntry.java.jinja
+++ b/ntcore/src/generate/main/java/NetworkTableEntry.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import java.nio.ByteBuffer;
@@ -608,3 +610,4 @@
   private final Topic m_topic;
   protected int m_handle;
 }
+
diff --git a/ntcore/src/generate/java/NetworkTableInstance.java.jinja b/ntcore/src/generate/main/java/NetworkTableInstance.java.jinja
similarity index 99%
rename from ntcore/src/generate/java/NetworkTableInstance.java.jinja
rename to ntcore/src/generate/main/java/NetworkTableInstance.java.jinja
index 9df129d..20861cd 100644
--- a/ntcore/src/generate/java/NetworkTableInstance.java.jinja
+++ b/ntcore/src/generate/main/java/NetworkTableInstance.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import edu.wpi.first.util.WPIUtilJNI;
@@ -1269,3 +1271,4 @@
   private int m_handle;
   private final ConcurrentMap<String, RawPublisher> m_schemas = new ConcurrentHashMap<>();
 }
+
diff --git a/ntcore/src/generate/java/NetworkTableValue.java.jinja b/ntcore/src/generate/main/java/NetworkTableValue.java.jinja
similarity index 98%
rename from ntcore/src/generate/java/NetworkTableValue.java.jinja
rename to ntcore/src/generate/main/java/NetworkTableValue.java.jinja
index d2c8d11..cb97570 100644
--- a/ntcore/src/generate/java/NetworkTableValue.java.jinja
+++ b/ntcore/src/generate/main/java/NetworkTableValue.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import java.util.Objects;
@@ -246,3 +248,4 @@
   private long m_time;
   private long m_serverTime;
 }
+
diff --git a/ntcore/src/generate/java/NetworkTablesJNI.java.jinja b/ntcore/src/generate/main/java/NetworkTablesJNI.java.jinja
similarity index 94%
rename from ntcore/src/generate/java/NetworkTablesJNI.java.jinja
rename to ntcore/src/generate/main/java/NetworkTablesJNI.java.jinja
index 6ff9c16..e16dbd2 100644
--- a/ntcore/src/generate/java/NetworkTablesJNI.java.jinja
+++ b/ntcore/src/generate/main/java/NetworkTablesJNI.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import edu.wpi.first.util.RuntimeLoader;
@@ -16,16 +18,30 @@
   static boolean libraryLoaded = false;
   static RuntimeLoader<NetworkTablesJNI> loader = null;
 
+  /** Sets whether JNI should be loaded in the static block. */
   public static class Helper {
     private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
 
+    /**
+     * Returns true if the JNI should be loaded in the static block.
+     *
+     * @return True if the JNI should be loaded in the static block.
+     */
     public static boolean getExtractOnStaticLoad() {
       return extractOnStaticLoad.get();
     }
 
+    /**
+     * Sets whether the JNI should be loaded in the static block.
+     *
+     * @param load Whether the JNI should be loaded in the static block.
+     */
     public static void setExtractOnStaticLoad(boolean load) {
       extractOnStaticLoad.set(load);
     }
+
+    /** Utility class. */
+    private Helper() {}
   }
 
   static {
@@ -121,6 +137,10 @@
 
   public static native boolean getTopicRetained(int topic);
 
+  public static native void setTopicCached(int topic, boolean value);
+
+  public static native boolean getTopicCached(int topic);
+
   public static native String getTopicTypeString(int topic);
 
   public static native boolean getTopicExists(int topic);
@@ -352,4 +372,8 @@
   public static native void stopConnectionDataLog(int logger);
 
   public static native int addLogger(int poller, int minLevel, int maxLevel);
+
+  /** Utility class. */
+  private NetworkTablesJNI() {}
 }
+
diff --git a/ntcore/src/generate/java/Publisher.java.jinja b/ntcore/src/generate/main/java/Publisher.java.jinja
similarity index 98%
rename from ntcore/src/generate/java/Publisher.java.jinja
rename to ntcore/src/generate/main/java/Publisher.java.jinja
index a403d91..d35c941 100644
--- a/ntcore/src/generate/java/Publisher.java.jinja
+++ b/ntcore/src/generate/main/java/Publisher.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 {% if TypeName == "Raw" %}
@@ -168,3 +170,4 @@
     set(value);
   }
 }
+
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generate/main/java/Subscriber.java.jinja
similarity index 96%
rename from ntcore/src/generate/java/Subscriber.java.jinja
rename to ntcore/src/generate/main/java/Subscriber.java.jinja
index 0ea09a3..73e6190 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generate/main/java/Subscriber.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
@@ -81,3 +83,4 @@
    */
   {{ java.ValueType }}[] readQueueValues();
 }
+
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generate/main/java/Timestamped.java.jinja
similarity index 93%
rename from ntcore/src/generate/java/Timestamped.java.jinja
rename to ntcore/src/generate/main/java/Timestamped.java.jinja
index 288af81..21e50e2 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generate/main/java/Timestamped.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /** NetworkTables timestamped {{ TypeName }}. */
@@ -38,3 +40,4 @@
   @SuppressWarnings("MemberName")
   public final {{ java.ValueType }} value;
 }
+
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generate/main/java/Topic.java.jinja
similarity index 98%
rename from ntcore/src/generate/java/Topic.java.jinja
rename to ntcore/src/generate/main/java/Topic.java.jinja
index e22fa3b..6407a4b 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generate/main/java/Topic.java.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /** NetworkTables {{ TypeName }} topic. */
@@ -221,3 +223,4 @@
   }
 {% endif %}
 }
+
diff --git a/ntcore/src/generate/cpp/jni/types_jni.cpp.jinja b/ntcore/src/generate/main/native/cpp/jni/types_jni.cpp.jinja
similarity index 98%
rename from ntcore/src/generate/cpp/jni/types_jni.cpp.jinja
rename to ntcore/src/generate/main/native/cpp/jni/types_jni.cpp.jinja
index 29cf570..fc811f5 100644
--- a/ntcore/src/generate/cpp/jni/types_jni.cpp.jinja
+++ b/ntcore/src/generate/main/native/cpp/jni/types_jni.cpp.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #include <jni.h>
 
 #include <wpi/jni_util.h>
@@ -367,3 +369,4 @@
 {% endif %}
 {% endfor %}
 }  // extern "C"
+
diff --git a/ntcore/src/generate/cpp/ntcore_c_types.cpp.jinja b/ntcore/src/generate/main/native/cpp/ntcore_c_types.cpp.jinja
similarity index 97%
rename from ntcore/src/generate/cpp/ntcore_c_types.cpp.jinja
rename to ntcore/src/generate/main/native/cpp/ntcore_c_types.cpp.jinja
index e74e5cf..a225634 100644
--- a/ntcore/src/generate/cpp/ntcore_c_types.cpp.jinja
+++ b/ntcore/src/generate/main/native/cpp/ntcore_c_types.cpp.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #include "ntcore_c_types.h"
 
 #include "Value_internal.h"
@@ -104,3 +106,4 @@
 
 {% endfor %}
 }  // extern "C"
+
diff --git a/ntcore/src/generate/cpp/ntcore_cpp_types.cpp.jinja b/ntcore/src/generate/main/native/cpp/ntcore_cpp_types.cpp.jinja
similarity index 97%
rename from ntcore/src/generate/cpp/ntcore_cpp_types.cpp.jinja
rename to ntcore/src/generate/main/native/cpp/ntcore_cpp_types.cpp.jinja
index cdaf27f..d9501f8 100644
--- a/ntcore/src/generate/cpp/ntcore_cpp_types.cpp.jinja
+++ b/ntcore/src/generate/main/native/cpp/ntcore_cpp_types.cpp.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #include "ntcore_cpp_types.h"
 
 #include "Handle.h"
@@ -129,3 +131,4 @@
 {% endif %}
 {% endfor %}
 }  // namespace nt
+
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generate/main/native/include/networktables/Topic.h.jinja
similarity index 99%
rename from ntcore/src/generate/include/networktables/Topic.h.jinja
rename to ntcore/src/generate/main/native/include/networktables/Topic.h.jinja
index ec2a915..93abc49 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generate/main/native/include/networktables/Topic.h.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
@@ -435,3 +437,4 @@
 }  // namespace nt
 
 #include "networktables/{{ TypeName }}Topic.inc"
+
diff --git a/ntcore/src/generate/include/networktables/Topic.inc.jinja b/ntcore/src/generate/main/native/include/networktables/Topic.inc.jinja
similarity index 97%
rename from ntcore/src/generate/include/networktables/Topic.inc.jinja
rename to ntcore/src/generate/main/native/include/networktables/Topic.inc.jinja
index 4e7a167..ce343d9 100644
--- a/ntcore/src/generate/include/networktables/Topic.inc.jinja
+++ b/ntcore/src/generate/main/native/include/networktables/Topic.inc.jinja
@@ -2,8 +2,12 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
+#include <vector>
+
 #include "networktables/{{ TypeName }}Topic.h"
 #include "networktables/NetworkTableType.h"
 #include "ntcore_cpp.h"
@@ -133,3 +137,4 @@
 }
 {% endif %}
 }  // namespace nt
+
diff --git a/ntcore/src/generate/include/ntcore_c_types.h.jinja b/ntcore/src/generate/main/native/include/ntcore_c_types.h.jinja
similarity index 97%
rename from ntcore/src/generate/include/ntcore_c_types.h.jinja
rename to ntcore/src/generate/main/native/include/ntcore_c_types.h.jinja
index 83cc807..22866fb 100644
--- a/ntcore/src/generate/include/ntcore_c_types.h.jinja
+++ b/ntcore/src/generate/main/native/include/ntcore_c_types.h.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
@@ -149,3 +151,4 @@
 #ifdef __cplusplus
 }  // extern "C"
 #endif
+
diff --git a/ntcore/src/generate/include/ntcore_cpp_types.h.jinja b/ntcore/src/generate/main/native/include/ntcore_cpp_types.h.jinja
similarity index 97%
rename from ntcore/src/generate/include/ntcore_cpp_types.h.jinja
rename to ntcore/src/generate/main/native/include/ntcore_cpp_types.h.jinja
index df919de..941414d 100644
--- a/ntcore/src/generate/include/ntcore_cpp_types.h.jinja
+++ b/ntcore/src/generate/main/native/include/ntcore_cpp_types.h.jinja
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
@@ -139,3 +141,4 @@
 /** @} */
 {% endfor %}
 }  // namespace nt
+
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayEntry.java
similarity index 65%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayEntry.java
index cbaa782..bcc72e0 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables BooleanArray entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface BooleanArrayEntry extends BooleanArraySubscriber, BooleanArrayPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayEntryImpl.java
new file mode 100644
index 0000000..40e455c
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables BooleanArray implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class BooleanArrayEntryImpl extends EntryBase implements BooleanArrayEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  BooleanArrayEntryImpl(BooleanArrayTopic topic, int handle, boolean[] defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public BooleanArrayTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public boolean[] get() {
+    return NetworkTablesJNI.getBooleanArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public boolean[] get(boolean[] defaultValue) {
+    return NetworkTablesJNI.getBooleanArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedBooleanArray getAtomic() {
+    return NetworkTablesJNI.getAtomicBooleanArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedBooleanArray getAtomic(boolean[] defaultValue) {
+    return NetworkTablesJNI.getAtomicBooleanArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedBooleanArray[] readQueue() {
+    return NetworkTablesJNI.readQueueBooleanArray(m_handle);
+  }
+
+  @Override
+  public boolean[][] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesBooleanArray(m_handle);
+  }
+
+  @Override
+  public void set(boolean[] value, long time) {
+    NetworkTablesJNI.setBooleanArray(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(boolean[] value) {
+    NetworkTablesJNI.setDefaultBooleanArray(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final BooleanArrayTopic m_topic;
+  private final boolean[] m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayPublisher.java
new file mode 100644
index 0000000..3e40bdb
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.Consumer;
+
+/** NetworkTables BooleanArray publisher. */
+public interface BooleanArrayPublisher extends Publisher, Consumer<boolean[]> {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  BooleanArrayTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(boolean[] value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(boolean[] value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(boolean[] value);
+
+  @Override
+  default void accept(boolean[] value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArraySubscriber.java
similarity index 70%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArraySubscriber.java
index 0ea09a3..ff07119 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArraySubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.Supplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables BooleanArray subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface BooleanArraySubscriber extends Subscriber, Supplier<boolean[]> {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  BooleanArrayTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  boolean[] get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,8 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
-  @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
-    return get();
-  }
-{% endif %}
+  boolean[] get(boolean[] defaultValue);
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +43,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedBooleanArray getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +53,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedBooleanArray getAtomic(boolean[] defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +65,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedBooleanArray[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +76,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  boolean[][] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayTopic.java
index e22fa3b..7d18f31 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanArrayTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables BooleanArray topic. */
+public final class BooleanArrayTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "boolean[]";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public BooleanArrayTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getBooleanArrayTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public BooleanArrayTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public BooleanArraySubscriber subscribe(
+      boolean[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kBooleanArray.getValue(),
+            "boolean[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public BooleanArraySubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      boolean[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kBooleanArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public BooleanArrayPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanArrayEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kBooleanArray.getValue(),
+            "boolean[]", options),
+        new boolean[] {});
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public BooleanArrayPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanArrayEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kBooleanArray.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        new boolean[] {});
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public BooleanArrayEntry getEntry(
+      boolean[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kBooleanArray.getValue(),
+            "boolean[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public BooleanArrayEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      boolean[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kBooleanArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanEntry.java
similarity index 67%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanEntry.java
index cbaa782..1821c06 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Boolean entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface BooleanEntry extends BooleanSubscriber, BooleanPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanEntryImpl.java
new file mode 100644
index 0000000..f099afc
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables Boolean implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class BooleanEntryImpl extends EntryBase implements BooleanEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  BooleanEntryImpl(BooleanTopic topic, int handle, boolean defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public BooleanTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public boolean get() {
+    return NetworkTablesJNI.getBoolean(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public boolean get(boolean defaultValue) {
+    return NetworkTablesJNI.getBoolean(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedBoolean getAtomic() {
+    return NetworkTablesJNI.getAtomicBoolean(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedBoolean getAtomic(boolean defaultValue) {
+    return NetworkTablesJNI.getAtomicBoolean(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedBoolean[] readQueue() {
+    return NetworkTablesJNI.readQueueBoolean(m_handle);
+  }
+
+  @Override
+  public boolean[] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesBoolean(m_handle);
+  }
+
+  @Override
+  public void set(boolean value, long time) {
+    NetworkTablesJNI.setBoolean(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(boolean value) {
+    NetworkTablesJNI.setDefaultBoolean(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final BooleanTopic m_topic;
+  private final boolean m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanPublisher.java
new file mode 100644
index 0000000..1254ff3
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import edu.wpi.first.util.function.BooleanConsumer;
+
+/** NetworkTables Boolean publisher. */
+public interface BooleanPublisher extends Publisher, BooleanConsumer {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  BooleanTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(boolean value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(boolean value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(boolean value);
+
+  @Override
+  default void accept(boolean value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanSubscriber.java
similarity index 71%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanSubscriber.java
index 0ea09a3..7f54f4b 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanSubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.BooleanSupplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables Boolean subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface BooleanSubscriber extends Subscriber, BooleanSupplier {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  BooleanTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  boolean get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,13 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
+  boolean get(boolean defaultValue);
+
   @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
+  default boolean getAsBoolean() {
     return get();
   }
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +48,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedBoolean getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +58,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedBoolean getAtomic(boolean defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +70,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedBoolean[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +81,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  boolean[] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanTopic.java
index e22fa3b..8ac0347 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/BooleanTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables Boolean topic. */
+public final class BooleanTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "boolean";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public BooleanTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getBooleanTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public BooleanTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public BooleanSubscriber subscribe(
+      boolean defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kBoolean.getValue(),
+            "boolean", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public BooleanSubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      boolean defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kBoolean.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public BooleanPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kBoolean.getValue(),
+            "boolean", options),
+        false);
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public BooleanPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kBoolean.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        false);
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public BooleanEntry getEntry(
+      boolean defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kBoolean.getValue(),
+            "boolean", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public BooleanEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      boolean defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new BooleanEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kBoolean.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayEntry.java
similarity index 65%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayEntry.java
index cbaa782..8a40d8b 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables DoubleArray entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface DoubleArrayEntry extends DoubleArraySubscriber, DoubleArrayPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayEntryImpl.java
new file mode 100644
index 0000000..7bb270a
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables DoubleArray implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class DoubleArrayEntryImpl extends EntryBase implements DoubleArrayEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  DoubleArrayEntryImpl(DoubleArrayTopic topic, int handle, double[] defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public DoubleArrayTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public double[] get() {
+    return NetworkTablesJNI.getDoubleArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public double[] get(double[] defaultValue) {
+    return NetworkTablesJNI.getDoubleArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedDoubleArray getAtomic() {
+    return NetworkTablesJNI.getAtomicDoubleArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedDoubleArray getAtomic(double[] defaultValue) {
+    return NetworkTablesJNI.getAtomicDoubleArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedDoubleArray[] readQueue() {
+    return NetworkTablesJNI.readQueueDoubleArray(m_handle);
+  }
+
+  @Override
+  public double[][] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesDoubleArray(m_handle);
+  }
+
+  @Override
+  public void set(double[] value, long time) {
+    NetworkTablesJNI.setDoubleArray(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(double[] value) {
+    NetworkTablesJNI.setDefaultDoubleArray(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final DoubleArrayTopic m_topic;
+  private final double[] m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayPublisher.java
new file mode 100644
index 0000000..39b367a
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.Consumer;
+
+/** NetworkTables DoubleArray publisher. */
+public interface DoubleArrayPublisher extends Publisher, Consumer<double[]> {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  DoubleArrayTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(double[] value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(double[] value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(double[] value);
+
+  @Override
+  default void accept(double[] value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArraySubscriber.java
similarity index 70%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArraySubscriber.java
index 0ea09a3..a807d66 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArraySubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.Supplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables DoubleArray subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface DoubleArraySubscriber extends Subscriber, Supplier<double[]> {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  DoubleArrayTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  double[] get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,8 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
-  @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
-    return get();
-  }
-{% endif %}
+  double[] get(double[] defaultValue);
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +43,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedDoubleArray getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +53,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedDoubleArray getAtomic(double[] defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +65,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedDoubleArray[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +76,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  double[][] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayTopic.java
index e22fa3b..f012869 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleArrayTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables DoubleArray topic. */
+public final class DoubleArrayTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "double[]";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public DoubleArrayTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getDoubleArrayTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public DoubleArrayTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public DoubleArraySubscriber subscribe(
+      double[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kDoubleArray.getValue(),
+            "double[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public DoubleArraySubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      double[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kDoubleArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public DoubleArrayPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleArrayEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kDoubleArray.getValue(),
+            "double[]", options),
+        new double[] {});
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public DoubleArrayPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleArrayEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kDoubleArray.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        new double[] {});
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public DoubleArrayEntry getEntry(
+      double[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kDoubleArray.getValue(),
+            "double[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public DoubleArrayEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      double[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kDoubleArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleEntry.java
similarity index 68%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleEntry.java
index cbaa782..b91e324 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Double entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface DoubleEntry extends DoubleSubscriber, DoublePublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleEntryImpl.java
new file mode 100644
index 0000000..968686d
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables Double implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class DoubleEntryImpl extends EntryBase implements DoubleEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  DoubleEntryImpl(DoubleTopic topic, int handle, double defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public DoubleTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public double get() {
+    return NetworkTablesJNI.getDouble(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public double get(double defaultValue) {
+    return NetworkTablesJNI.getDouble(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedDouble getAtomic() {
+    return NetworkTablesJNI.getAtomicDouble(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedDouble getAtomic(double defaultValue) {
+    return NetworkTablesJNI.getAtomicDouble(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedDouble[] readQueue() {
+    return NetworkTablesJNI.readQueueDouble(m_handle);
+  }
+
+  @Override
+  public double[] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesDouble(m_handle);
+  }
+
+  @Override
+  public void set(double value, long time) {
+    NetworkTablesJNI.setDouble(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(double value) {
+    NetworkTablesJNI.setDefaultDouble(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final DoubleTopic m_topic;
+  private final double m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoublePublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoublePublisher.java
new file mode 100644
index 0000000..d477921
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoublePublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.DoubleConsumer;
+
+/** NetworkTables Double publisher. */
+public interface DoublePublisher extends Publisher, DoubleConsumer {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  DoubleTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(double value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(double value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(double value);
+
+  @Override
+  default void accept(double value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleSubscriber.java
similarity index 71%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleSubscriber.java
index 0ea09a3..688f6ea 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleSubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.DoubleSupplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables Double subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface DoubleSubscriber extends Subscriber, DoubleSupplier {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  DoubleTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  double get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,13 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
+  double get(double defaultValue);
+
   @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
+  default double getAsDouble() {
     return get();
   }
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +48,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedDouble getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +58,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedDouble getAtomic(double defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +70,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedDouble[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +81,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  double[] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleTopic.java
index e22fa3b..2456841 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/DoubleTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables Double topic. */
+public final class DoubleTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "double";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public DoubleTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getDoubleTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public DoubleTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public DoubleSubscriber subscribe(
+      double defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kDouble.getValue(),
+            "double", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public DoubleSubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      double defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kDouble.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public DoublePublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kDouble.getValue(),
+            "double", options),
+        0);
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public DoublePublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kDouble.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        0);
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public DoubleEntry getEntry(
+      double defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kDouble.getValue(),
+            "double", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public DoubleEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      double defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new DoubleEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kDouble.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayEntry.java
similarity index 66%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayEntry.java
index cbaa782..b4cd353 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables FloatArray entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface FloatArrayEntry extends FloatArraySubscriber, FloatArrayPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayEntryImpl.java
new file mode 100644
index 0000000..1afe837
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables FloatArray implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class FloatArrayEntryImpl extends EntryBase implements FloatArrayEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  FloatArrayEntryImpl(FloatArrayTopic topic, int handle, float[] defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public FloatArrayTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public float[] get() {
+    return NetworkTablesJNI.getFloatArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public float[] get(float[] defaultValue) {
+    return NetworkTablesJNI.getFloatArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedFloatArray getAtomic() {
+    return NetworkTablesJNI.getAtomicFloatArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedFloatArray getAtomic(float[] defaultValue) {
+    return NetworkTablesJNI.getAtomicFloatArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedFloatArray[] readQueue() {
+    return NetworkTablesJNI.readQueueFloatArray(m_handle);
+  }
+
+  @Override
+  public float[][] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesFloatArray(m_handle);
+  }
+
+  @Override
+  public void set(float[] value, long time) {
+    NetworkTablesJNI.setFloatArray(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(float[] value) {
+    NetworkTablesJNI.setDefaultFloatArray(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final FloatArrayTopic m_topic;
+  private final float[] m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayPublisher.java
new file mode 100644
index 0000000..afaf9f2
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.Consumer;
+
+/** NetworkTables FloatArray publisher. */
+public interface FloatArrayPublisher extends Publisher, Consumer<float[]> {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  FloatArrayTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(float[] value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(float[] value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(float[] value);
+
+  @Override
+  default void accept(float[] value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArraySubscriber.java
similarity index 70%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArraySubscriber.java
index 0ea09a3..b70bece 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArraySubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.Supplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables FloatArray subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface FloatArraySubscriber extends Subscriber, Supplier<float[]> {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  FloatArrayTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  float[] get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,8 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
-  @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
-    return get();
-  }
-{% endif %}
+  float[] get(float[] defaultValue);
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +43,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedFloatArray getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +53,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedFloatArray getAtomic(float[] defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +65,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedFloatArray[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +76,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  float[][] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayTopic.java
index e22fa3b..e20ea7f 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatArrayTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables FloatArray topic. */
+public final class FloatArrayTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "float[]";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public FloatArrayTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getFloatArrayTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public FloatArrayTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public FloatArraySubscriber subscribe(
+      float[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kFloatArray.getValue(),
+            "float[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public FloatArraySubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      float[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kFloatArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public FloatArrayPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatArrayEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kFloatArray.getValue(),
+            "float[]", options),
+        new float[] {});
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public FloatArrayPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatArrayEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kFloatArray.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        new float[] {});
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public FloatArrayEntry getEntry(
+      float[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kFloatArray.getValue(),
+            "float[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public FloatArrayEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      float[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kFloatArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatEntry.java
similarity index 68%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatEntry.java
index cbaa782..9830123 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Float entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface FloatEntry extends FloatSubscriber, FloatPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatEntryImpl.java
new file mode 100644
index 0000000..f7efebf
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables Float implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class FloatEntryImpl extends EntryBase implements FloatEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  FloatEntryImpl(FloatTopic topic, int handle, float defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public FloatTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public float get() {
+    return NetworkTablesJNI.getFloat(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public float get(float defaultValue) {
+    return NetworkTablesJNI.getFloat(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedFloat getAtomic() {
+    return NetworkTablesJNI.getAtomicFloat(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedFloat getAtomic(float defaultValue) {
+    return NetworkTablesJNI.getAtomicFloat(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedFloat[] readQueue() {
+    return NetworkTablesJNI.readQueueFloat(m_handle);
+  }
+
+  @Override
+  public float[] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesFloat(m_handle);
+  }
+
+  @Override
+  public void set(float value, long time) {
+    NetworkTablesJNI.setFloat(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(float value) {
+    NetworkTablesJNI.setDefaultFloat(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final FloatTopic m_topic;
+  private final float m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatPublisher.java
new file mode 100644
index 0000000..3f4b320
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import edu.wpi.first.util.function.FloatConsumer;
+
+/** NetworkTables Float publisher. */
+public interface FloatPublisher extends Publisher, FloatConsumer {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  FloatTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(float value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(float value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(float value);
+
+  @Override
+  default void accept(float value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatSubscriber.java
similarity index 71%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatSubscriber.java
index 0ea09a3..758463b 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatSubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import edu.wpi.first.util.function.FloatSupplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables Float subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface FloatSubscriber extends Subscriber, FloatSupplier {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  FloatTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  float get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,13 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
+  float get(float defaultValue);
+
   @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
+  default float getAsFloat() {
     return get();
   }
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +48,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedFloat getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +58,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedFloat getAtomic(float defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +70,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedFloat[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +81,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  float[] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatTopic.java
index e22fa3b..2ac7c7c 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/FloatTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables Float topic. */
+public final class FloatTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "float";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public FloatTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getFloatTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public FloatTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public FloatSubscriber subscribe(
+      float defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kFloat.getValue(),
+            "float", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public FloatSubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      float defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kFloat.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public FloatPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kFloat.getValue(),
+            "float", options),
+        0);
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public FloatPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kFloat.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        0);
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public FloatEntry getEntry(
+      float defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kFloat.getValue(),
+            "float", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public FloatEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      float defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new FloatEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kFloat.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericEntryImpl.java
new file mode 100644
index 0000000..b467c83
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericEntryImpl.java
@@ -0,0 +1,815 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.nio.ByteBuffer;
+
+/** NetworkTables generic implementation. */
+final class GenericEntryImpl extends EntryBase implements GenericEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   */
+  GenericEntryImpl(Topic topic, int handle) {
+    super(handle);
+    m_topic = topic;
+  }
+
+  @Override
+  public Topic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public NetworkTableValue get() {
+    return NetworkTablesJNI.getValue(m_handle);
+  }
+
+  /**
+   * Gets the entry's value as a boolean. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public boolean getBoolean(boolean defaultValue) {
+    return NetworkTablesJNI.getBoolean(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a long. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public long getInteger(long defaultValue) {
+    return NetworkTablesJNI.getInteger(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a float. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public float getFloat(float defaultValue) {
+    return NetworkTablesJNI.getFloat(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a double. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public double getDouble(double defaultValue) {
+    return NetworkTablesJNI.getDouble(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a String. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public String getString(String defaultValue) {
+    return NetworkTablesJNI.getString(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a byte[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public byte[] getRaw(byte[] defaultValue) {
+    return NetworkTablesJNI.getRaw(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public boolean[] getBooleanArray(boolean[] defaultValue) {
+    return NetworkTablesJNI.getBooleanArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public Boolean[] getBooleanArray(Boolean[] defaultValue) {
+    return NetworkTableValue.fromNativeBooleanArray(
+        getBooleanArray(NetworkTableValue.toNativeBooleanArray(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a long[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public long[] getIntegerArray(long[] defaultValue) {
+    return NetworkTablesJNI.getIntegerArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public Long[] getIntegerArray(Long[] defaultValue) {
+    return NetworkTableValue.fromNativeIntegerArray(
+        getIntegerArray(NetworkTableValue.toNativeIntegerArray(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a float[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public float[] getFloatArray(float[] defaultValue) {
+    return NetworkTablesJNI.getFloatArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public Float[] getFloatArray(Float[] defaultValue) {
+    return NetworkTableValue.fromNativeFloatArray(
+        getFloatArray(NetworkTableValue.toNativeFloatArray(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a double[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public double[] getDoubleArray(double[] defaultValue) {
+    return NetworkTablesJNI.getDoubleArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public Double[] getDoubleArray(Double[] defaultValue) {
+    return NetworkTableValue.fromNativeDoubleArray(
+        getDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a String[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  @Override
+  public String[] getStringArray(String[] defaultValue) {
+    return NetworkTablesJNI.getStringArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public NetworkTableValue[] readQueue() {
+    return NetworkTablesJNI.readQueueValue(m_handle);
+  }
+
+  @Override
+  public boolean set(NetworkTableValue value) {
+    long time = value.getTime();
+    Object otherValue = value.getValue();
+    switch (value.getType()) {
+      case kBoolean:
+        return NetworkTablesJNI.setBoolean(m_handle, time, (Boolean) otherValue);
+      case kInteger:
+        return NetworkTablesJNI.setInteger(
+            m_handle, time, ((Number) otherValue).longValue());
+      case kFloat:
+        return NetworkTablesJNI.setFloat(
+            m_handle, time, ((Number) otherValue).floatValue());
+      case kDouble:
+        return NetworkTablesJNI.setDouble(
+            m_handle, time, ((Number) otherValue).doubleValue());
+      case kString:
+        return NetworkTablesJNI.setString(m_handle, time, (String) otherValue);
+      case kRaw:
+        return NetworkTablesJNI.setRaw(m_handle, time, (byte[]) otherValue);
+      case kBooleanArray:
+        return NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[]) otherValue);
+      case kIntegerArray:
+        return NetworkTablesJNI.setIntegerArray(m_handle, time, (long[]) otherValue);
+      case kFloatArray:
+        return NetworkTablesJNI.setFloatArray(m_handle, time, (float[]) otherValue);
+      case kDoubleArray:
+        return NetworkTablesJNI.setDoubleArray(m_handle, time, (double[]) otherValue);
+      case kStringArray:
+        return NetworkTablesJNI.setStringArray(m_handle, time, (String[]) otherValue);
+      default:
+        return true;
+    }
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  @Override
+  public boolean setValue(Object value, long time) {
+    if (value instanceof NetworkTableValue) {
+      return set((NetworkTableValue) value);
+    } else if (value instanceof Boolean) {
+      return setBoolean((Boolean) value, time);
+    } else if (value instanceof Long) {
+      return setInteger((Long) value, time);
+    } else if (value instanceof Float) {
+      return setFloat((Float) value, time);
+    } else if (value instanceof Number) {
+      return setNumber((Number) value, time);
+    } else if (value instanceof String) {
+      return setString((String) value, time);
+    } else if (value instanceof byte[]) {
+      return setRaw((byte[]) value, time);
+    } else if (value instanceof boolean[]) {
+      return setBooleanArray((boolean[]) value, time);
+    } else if (value instanceof long[]) {
+      return setIntegerArray((long[]) value, time);
+    } else if (value instanceof float[]) {
+      return setFloatArray((float[]) value, time);
+    } else if (value instanceof double[]) {
+      return setDoubleArray((double[]) value, time);
+    } else if (value instanceof Boolean[]) {
+      return setBooleanArray((Boolean[]) value, time);
+    } else if (value instanceof Long[]) {
+      return setIntegerArray((Long[]) value, time);
+    } else if (value instanceof Float[]) {
+      return setFloatArray((Float[]) value, time);
+    } else if (value instanceof Number[]) {
+      return setNumberArray((Number[]) value, time);
+    } else if (value instanceof String[]) {
+      return setStringArray((String[]) value, time);
+    } else {
+      throw new IllegalArgumentException(
+          "Value of type " + value.getClass().getName() + " cannot be put into a table");
+    }
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setBoolean(boolean value, long time) {
+    return NetworkTablesJNI.setBoolean(m_handle, time, value);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setInteger(long value, long time) {
+    return NetworkTablesJNI.setInteger(m_handle, time, value);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setFloat(float value, long time) {
+    return NetworkTablesJNI.setFloat(m_handle, time, value);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDouble(double value, long time) {
+    return NetworkTablesJNI.setDouble(m_handle, time, value);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setString(String value, long time) {
+    return NetworkTablesJNI.setString(m_handle, time, value);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.length - start)
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setRaw(byte[] value, int start, int len, long time) {
+    return NetworkTablesJNI.setRaw(m_handle, time, value, start, len);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.capacity() - start)
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setRaw(ByteBuffer value, int start, int len, long time) {
+    return NetworkTablesJNI.setRaw(m_handle, time, value, start, len);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setBooleanArray(boolean[] value, long time) {
+    return NetworkTablesJNI.setBooleanArray(m_handle, time, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setBooleanArray(Boolean[] value, long time) {
+    return setBooleanArray(NetworkTableValue.toNativeBooleanArray(value), time);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setIntegerArray(long[] value, long time) {
+    return NetworkTablesJNI.setIntegerArray(m_handle, time, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setIntegerArray(Long[] value, long time) {
+    return setIntegerArray(NetworkTableValue.toNativeIntegerArray(value), time);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setFloatArray(float[] value, long time) {
+    return NetworkTablesJNI.setFloatArray(m_handle, time, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setFloatArray(Float[] value, long time) {
+    return setFloatArray(NetworkTableValue.toNativeFloatArray(value), time);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDoubleArray(double[] value, long time) {
+    return NetworkTablesJNI.setDoubleArray(m_handle, time, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDoubleArray(Double[] value, long time) {
+    return setDoubleArray(NetworkTableValue.toNativeDoubleArray(value), time);
+  }
+
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setStringArray(String[] value, long time) {
+    return NetworkTablesJNI.setStringArray(m_handle, time, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setNumber(Number value, long time) {
+    return setDouble(value.doubleValue(), time);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setNumberArray(Number[] value, long time) {
+    return setDoubleArray(NetworkTableValue.toNativeDoubleArray(value), time);
+  }
+
+  @Override
+  public boolean setDefault(NetworkTableValue defaultValue) {
+    long time = defaultValue.getTime();
+    Object otherValue = defaultValue.getValue();
+    switch (defaultValue.getType()) {
+      case kBoolean:
+        return NetworkTablesJNI.setDefaultBoolean(m_handle, time, (Boolean) otherValue);
+      case kInteger:
+        return NetworkTablesJNI.setDefaultInteger(
+            m_handle, time, ((Number) otherValue).longValue());
+      case kFloat:
+        return NetworkTablesJNI.setDefaultFloat(
+            m_handle, time, ((Number) otherValue).floatValue());
+      case kDouble:
+        return NetworkTablesJNI.setDefaultDouble(
+            m_handle, time, ((Number) otherValue).doubleValue());
+      case kString:
+        return NetworkTablesJNI.setDefaultString(m_handle, time, (String) otherValue);
+      case kRaw:
+        return NetworkTablesJNI.setDefaultRaw(m_handle, time, (byte[]) otherValue);
+      case kBooleanArray:
+        return NetworkTablesJNI.setDefaultBooleanArray(m_handle, time, (boolean[]) otherValue);
+      case kIntegerArray:
+        return NetworkTablesJNI.setDefaultIntegerArray(m_handle, time, (long[]) otherValue);
+      case kFloatArray:
+        return NetworkTablesJNI.setDefaultFloatArray(m_handle, time, (float[]) otherValue);
+      case kDoubleArray:
+        return NetworkTablesJNI.setDefaultDoubleArray(m_handle, time, (double[]) otherValue);
+      case kStringArray:
+        return NetworkTablesJNI.setDefaultStringArray(m_handle, time, (String[]) otherValue);
+      default:
+        return true;
+    }
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  @Override
+  public boolean setDefaultValue(Object defaultValue) {
+    if (defaultValue instanceof NetworkTableValue) {
+      return setDefault((NetworkTableValue) defaultValue);
+    } else if (defaultValue instanceof Boolean) {
+      return setDefaultBoolean((Boolean) defaultValue);
+    } else if (defaultValue instanceof Integer) {
+      return setDefaultInteger((Integer) defaultValue);
+    } else if (defaultValue instanceof Float) {
+      return setDefaultFloat((Float) defaultValue);
+    } else if (defaultValue instanceof Number) {
+      return setDefaultNumber((Number) defaultValue);
+    } else if (defaultValue instanceof String) {
+      return setDefaultString((String) defaultValue);
+    } else if (defaultValue instanceof byte[]) {
+      return setDefaultRaw((byte[]) defaultValue);
+    } else if (defaultValue instanceof boolean[]) {
+      return setDefaultBooleanArray((boolean[]) defaultValue);
+    } else if (defaultValue instanceof long[]) {
+      return setDefaultIntegerArray((long[]) defaultValue);
+    } else if (defaultValue instanceof float[]) {
+      return setDefaultFloatArray((float[]) defaultValue);
+    } else if (defaultValue instanceof double[]) {
+      return setDefaultDoubleArray((double[]) defaultValue);
+    } else if (defaultValue instanceof Boolean[]) {
+      return setDefaultBooleanArray((Boolean[]) defaultValue);
+    } else if (defaultValue instanceof Long[]) {
+      return setDefaultIntegerArray((Long[]) defaultValue);
+    } else if (defaultValue instanceof Float[]) {
+      return setDefaultFloatArray((Float[]) defaultValue);
+    } else if (defaultValue instanceof Number[]) {
+      return setDefaultNumberArray((Number[]) defaultValue);
+    } else if (defaultValue instanceof String[]) {
+      return setDefaultStringArray((String[]) defaultValue);
+    } else {
+      throw new IllegalArgumentException(
+          "Value of type " + defaultValue.getClass().getName() + " cannot be put into a table");
+    }
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultBoolean(boolean defaultValue) {
+    return NetworkTablesJNI.setDefaultBoolean(m_handle, 0, defaultValue);
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultInteger(long defaultValue) {
+    return NetworkTablesJNI.setDefaultInteger(m_handle, 0, defaultValue);
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultFloat(float defaultValue) {
+    return NetworkTablesJNI.setDefaultFloat(m_handle, 0, defaultValue);
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultDouble(double defaultValue) {
+    return NetworkTablesJNI.setDefaultDouble(m_handle, 0, defaultValue);
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultString(String defaultValue) {
+    return NetworkTablesJNI.setDefaultString(m_handle, 0, defaultValue);
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.length - start)
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultRaw(byte[] defaultValue, int start, int len) {
+    return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue, start, len);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.capacity() - start)
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultRaw(ByteBuffer defaultValue, int start, int len) {
+    return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue, start, len);
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultBooleanArray(boolean[] defaultValue) {
+    return NetworkTablesJNI.setDefaultBooleanArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultBooleanArray(Boolean[] defaultValue) {
+    return setDefaultBooleanArray(NetworkTableValue.toNativeBooleanArray(defaultValue));
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultIntegerArray(long[] defaultValue) {
+    return NetworkTablesJNI.setDefaultIntegerArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultIntegerArray(Long[] defaultValue) {
+    return setDefaultIntegerArray(NetworkTableValue.toNativeIntegerArray(defaultValue));
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultFloatArray(float[] defaultValue) {
+    return NetworkTablesJNI.setDefaultFloatArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultFloatArray(Float[] defaultValue) {
+    return setDefaultFloatArray(NetworkTableValue.toNativeFloatArray(defaultValue));
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultDoubleArray(double[] defaultValue) {
+    return NetworkTablesJNI.setDefaultDoubleArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultDoubleArray(Double[] defaultValue) {
+    return setDefaultDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue));
+  }
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  @Override
+  public boolean setDefaultStringArray(String[] defaultValue) {
+    return NetworkTablesJNI.setDefaultStringArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultNumber(Number defaultValue) {
+    return setDefaultDouble(defaultValue.doubleValue());
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultNumberArray(Number[] defaultValue) {
+    return setDefaultDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue));
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final Topic m_topic;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericPublisher.java
new file mode 100644
index 0000000..8fd03ec
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericPublisher.java
@@ -0,0 +1,568 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.nio.ByteBuffer;
+import java.util.function.Consumer;
+
+/** NetworkTables generic publisher. */
+public interface GenericPublisher extends Publisher, Consumer<NetworkTableValue> {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  Topic getTopic();
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  boolean set(NetworkTableValue value);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  default boolean setValue(Object value) {
+    return setValue(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  boolean setValue(Object value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setBoolean(boolean value) {
+    return setBoolean(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setBoolean(boolean value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setInteger(long value) {
+    return setInteger(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setInteger(long value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setFloat(float value) {
+    return setFloat(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setFloat(float value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setDouble(double value) {
+    return setDouble(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setDouble(double value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setString(String value) {
+    return setString(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setString(String value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setRaw(byte[] value) {
+    return setRaw(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setRaw(ByteBuffer value) {
+    return setRaw(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setRaw(byte[] value, long time) {
+    return setRaw(value, 0, value.length, time);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish; will send from value.position() to value.limit()
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setRaw(ByteBuffer value, long time) {
+    int pos = value.position();
+    return setRaw(value, pos, value.limit() - pos, time);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.length - start)
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setRaw(byte[] value, int start, int len) {
+    return setRaw(value, start, len, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.length - start)
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setRaw(byte[] value, int start, int len, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.capacity() - start)
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setRaw(ByteBuffer value, int start, int len) {
+    return setRaw(value, start, len, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.capacity() - start)
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setRaw(ByteBuffer value, int start, int len, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setBooleanArray(boolean[] value) {
+    return setBooleanArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setBooleanArray(boolean[] value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setBooleanArray(Boolean[] value) {
+    return setBooleanArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setBooleanArray(Boolean[] value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setIntegerArray(long[] value) {
+    return setIntegerArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setIntegerArray(long[] value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setIntegerArray(Long[] value) {
+    return setIntegerArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setIntegerArray(Long[] value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setFloatArray(float[] value) {
+    return setFloatArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setFloatArray(float[] value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setFloatArray(Float[] value) {
+    return setFloatArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setFloatArray(Float[] value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setDoubleArray(double[] value) {
+    return setDoubleArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setDoubleArray(double[] value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setDoubleArray(Double[] value) {
+    return setDoubleArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setDoubleArray(Double[] value, long time);
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @return False if the topic already exists with a different type
+   */
+  default boolean setStringArray(String[] value) {
+    return setStringArray(value, 0);
+  }
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   * @return False if the topic already exists with a different type
+   */
+  boolean setStringArray(String[] value, long time);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefault(NetworkTableValue defaultValue);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  boolean setDefaultValue(Object defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultBoolean(boolean defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultInteger(long defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultFloat(float defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultDouble(double defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultString(String defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  default boolean setDefaultRaw(byte[] defaultValue) {
+    return setDefaultRaw(defaultValue, 0, defaultValue.length);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set; will send from defaultValue.position() to
+   *                     defaultValue.limit()
+   * @return False if the entry exists with a different type
+   */
+  default boolean setDefaultRaw(ByteBuffer defaultValue) {
+    int pos = defaultValue.position();
+    return setDefaultRaw(defaultValue, pos, defaultValue.limit() - pos);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.length - start)
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultRaw(byte[] defaultValue, int start, int len);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.capacity() - start)
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultRaw(ByteBuffer defaultValue, int start, int len);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultBooleanArray(boolean[] defaultValue);
+
+  boolean setDefaultBooleanArray(Boolean[] defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultIntegerArray(long[] defaultValue);
+
+  boolean setDefaultIntegerArray(Long[] defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultFloatArray(float[] defaultValue);
+
+  boolean setDefaultFloatArray(Float[] defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultDoubleArray(double[] defaultValue);
+
+  boolean setDefaultDoubleArray(Double[] defaultValue);
+
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  boolean setDefaultStringArray(String[] defaultValue);
+
+  @Override
+  default void accept(NetworkTableValue value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericSubscriber.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericSubscriber.java
new file mode 100644
index 0000000..7d7ca96
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/GenericSubscriber.java
@@ -0,0 +1,176 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.Supplier;
+
+/** NetworkTables generic subscriber. */
+@SuppressWarnings("PMD.MissingOverride")
+public interface GenericSubscriber extends Subscriber, Supplier<NetworkTableValue> {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  Topic getTopic();
+
+  /**
+   * Get the last published value.
+   * If no value has been published, returns a value with type NetworkTableType.kUnassigned.
+   *
+   * @return value
+   */
+  NetworkTableValue get();
+
+  /**
+   * Gets the entry's value as a boolean. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  boolean getBoolean(boolean defaultValue);
+
+  /**
+   * Gets the entry's value as a long. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  long getInteger(long defaultValue);
+
+  /**
+   * Gets the entry's value as a float. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  float getFloat(float defaultValue);
+
+  /**
+   * Gets the entry's value as a double. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  double getDouble(double defaultValue);
+
+  /**
+   * Gets the entry's value as a String. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  String getString(String defaultValue);
+
+  /**
+   * Gets the entry's value as a byte[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  byte[] getRaw(byte[] defaultValue);
+
+  /**
+   * Gets the entry's value as a boolean[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  boolean[] getBooleanArray(boolean[] defaultValue);
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  Boolean[] getBooleanArray(Boolean[] defaultValue);
+
+  /**
+   * Gets the entry's value as a long[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  long[] getIntegerArray(long[] defaultValue);
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  Long[] getIntegerArray(Long[] defaultValue);
+
+  /**
+   * Gets the entry's value as a float[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  float[] getFloatArray(float[] defaultValue);
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  Float[] getFloatArray(Float[] defaultValue);
+
+  /**
+   * Gets the entry's value as a double[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  double[] getDoubleArray(double[] defaultValue);
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  Double[] getDoubleArray(Double[] defaultValue);
+
+  /**
+   * Gets the entry's value as a String[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  String[] getStringArray(String[] defaultValue);
+
+  /**
+   * Get an array of all value changes since the last call to readQueue.
+   * Also provides a timestamp for each value.
+   *
+   * <p>The "poll storage" subscribe option can be used to set the queue
+   * depth.
+   *
+   * @return Array of timestamped values; empty array if no new changes have
+   *     been published since the previous call.
+   */
+  NetworkTableValue[] readQueue();
+}
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayEntry.java
similarity index 65%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayEntry.java
index cbaa782..fd8f9c2 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables IntegerArray entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface IntegerArrayEntry extends IntegerArraySubscriber, IntegerArrayPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayEntryImpl.java
new file mode 100644
index 0000000..e74f489
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables IntegerArray implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class IntegerArrayEntryImpl extends EntryBase implements IntegerArrayEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  IntegerArrayEntryImpl(IntegerArrayTopic topic, int handle, long[] defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public IntegerArrayTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public long[] get() {
+    return NetworkTablesJNI.getIntegerArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public long[] get(long[] defaultValue) {
+    return NetworkTablesJNI.getIntegerArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedIntegerArray getAtomic() {
+    return NetworkTablesJNI.getAtomicIntegerArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedIntegerArray getAtomic(long[] defaultValue) {
+    return NetworkTablesJNI.getAtomicIntegerArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedIntegerArray[] readQueue() {
+    return NetworkTablesJNI.readQueueIntegerArray(m_handle);
+  }
+
+  @Override
+  public long[][] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesIntegerArray(m_handle);
+  }
+
+  @Override
+  public void set(long[] value, long time) {
+    NetworkTablesJNI.setIntegerArray(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(long[] value) {
+    NetworkTablesJNI.setDefaultIntegerArray(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final IntegerArrayTopic m_topic;
+  private final long[] m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayPublisher.java
new file mode 100644
index 0000000..b864ba1
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.Consumer;
+
+/** NetworkTables IntegerArray publisher. */
+public interface IntegerArrayPublisher extends Publisher, Consumer<long[]> {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  IntegerArrayTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(long[] value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(long[] value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(long[] value);
+
+  @Override
+  default void accept(long[] value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArraySubscriber.java
similarity index 70%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArraySubscriber.java
index 0ea09a3..cbb2e6f 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArraySubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.Supplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables IntegerArray subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface IntegerArraySubscriber extends Subscriber, Supplier<long[]> {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  IntegerArrayTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  long[] get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,8 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
-  @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
-    return get();
-  }
-{% endif %}
+  long[] get(long[] defaultValue);
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +43,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedIntegerArray getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +53,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedIntegerArray getAtomic(long[] defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +65,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedIntegerArray[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +76,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  long[][] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayTopic.java
index e22fa3b..d020e8a 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerArrayTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables IntegerArray topic. */
+public final class IntegerArrayTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "int[]";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public IntegerArrayTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getIntegerArrayTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public IntegerArrayTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public IntegerArraySubscriber subscribe(
+      long[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kIntegerArray.getValue(),
+            "int[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public IntegerArraySubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      long[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kIntegerArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public IntegerArrayPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerArrayEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kIntegerArray.getValue(),
+            "int[]", options),
+        new long[] {});
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public IntegerArrayPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerArrayEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kIntegerArray.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        new long[] {});
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public IntegerArrayEntry getEntry(
+      long[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kIntegerArray.getValue(),
+            "int[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public IntegerArrayEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      long[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kIntegerArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerEntry.java
similarity index 67%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerEntry.java
index cbaa782..ccd614e 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Integer entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface IntegerEntry extends IntegerSubscriber, IntegerPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerEntryImpl.java
new file mode 100644
index 0000000..a8db1bf
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables Integer implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class IntegerEntryImpl extends EntryBase implements IntegerEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  IntegerEntryImpl(IntegerTopic topic, int handle, long defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public IntegerTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public long get() {
+    return NetworkTablesJNI.getInteger(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public long get(long defaultValue) {
+    return NetworkTablesJNI.getInteger(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedInteger getAtomic() {
+    return NetworkTablesJNI.getAtomicInteger(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedInteger getAtomic(long defaultValue) {
+    return NetworkTablesJNI.getAtomicInteger(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedInteger[] readQueue() {
+    return NetworkTablesJNI.readQueueInteger(m_handle);
+  }
+
+  @Override
+  public long[] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesInteger(m_handle);
+  }
+
+  @Override
+  public void set(long value, long time) {
+    NetworkTablesJNI.setInteger(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(long value) {
+    NetworkTablesJNI.setDefaultInteger(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final IntegerTopic m_topic;
+  private final long m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerPublisher.java
new file mode 100644
index 0000000..be734ed
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.LongConsumer;
+
+/** NetworkTables Integer publisher. */
+public interface IntegerPublisher extends Publisher, LongConsumer {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  IntegerTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(long value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(long value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(long value);
+
+  @Override
+  default void accept(long value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerSubscriber.java
similarity index 71%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerSubscriber.java
index 0ea09a3..81a9740 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerSubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.LongSupplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables Integer subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface IntegerSubscriber extends Subscriber, LongSupplier {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  IntegerTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  long get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,13 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
+  long get(long defaultValue);
+
   @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
+  default long getAsLong() {
     return get();
   }
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +48,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedInteger getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +58,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedInteger getAtomic(long defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +70,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedInteger[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +81,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  long[] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerTopic.java
index e22fa3b..ebf87c4 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/IntegerTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables Integer topic. */
+public final class IntegerTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "int";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public IntegerTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getIntegerTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public IntegerTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public IntegerSubscriber subscribe(
+      long defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kInteger.getValue(),
+            "int", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public IntegerSubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      long defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kInteger.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public IntegerPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kInteger.getValue(),
+            "int", options),
+        0);
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public IntegerPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kInteger.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        0);
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public IntegerEntry getEntry(
+      long defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kInteger.getValue(),
+            "int", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public IntegerEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      long defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new IntegerEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kInteger.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableEntry.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableEntry.java
new file mode 100644
index 0000000..69d1077
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableEntry.java
@@ -0,0 +1,1013 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.nio.ByteBuffer;
+
+/**
+ * NetworkTables Entry.
+ *
+ * <p>For backwards compatibility, the NetworkTableEntry close() does not release the entry.
+ */
+@SuppressWarnings("UnnecessaryParentheses")
+public final class NetworkTableEntry implements Publisher, Subscriber {
+  /**
+   * Flag values (as returned by {@link #getFlags()}).
+   *
+   * @deprecated Use isPersistent() instead.
+   */
+  @Deprecated(since = "2022", forRemoval = true)
+  public static final int kPersistent = 0x01;
+
+  /**
+   * Construct from native handle.
+   *
+   * @param inst Instance
+   * @param handle Native handle
+   */
+  public NetworkTableEntry(NetworkTableInstance inst, int handle) {
+    this(new Topic(inst, NetworkTablesJNI.getTopicFromHandle(handle)), handle);
+  }
+
+  /**
+   * Construct from native handle.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   */
+  public NetworkTableEntry(Topic topic, int handle) {
+    m_topic = topic;
+    m_handle = handle;
+  }
+
+  @Override
+  public void close() {}
+
+  /**
+   * Determines if the native handle is valid.
+   *
+   * @return True if the native handle is valid, false otherwise.
+   */
+  @Override
+  public boolean isValid() {
+    return m_handle != 0;
+  }
+
+  /**
+   * Gets the native handle for the entry.
+   *
+   * @return Native handle
+   */
+  @Override
+  public int getHandle() {
+    return m_handle;
+  }
+
+  /**
+   * Gets the subscribed-to / published-to topic.
+   *
+   * @return Topic
+   */
+  @Override
+  public Topic getTopic() {
+    return m_topic;
+  }
+
+  /**
+   * Gets the instance for the entry.
+   *
+   * @return Instance
+   */
+  public NetworkTableInstance getInstance() {
+    return m_topic.getInstance();
+  }
+
+  /**
+   * Determines if the entry currently exists.
+   *
+   * @return True if the entry exists, false otherwise.
+   */
+  @Override
+  public boolean exists() {
+    return NetworkTablesJNI.getType(m_handle) != 0;
+  }
+
+  /**
+   * Gets the name of the entry (the key).
+   *
+   * @return the entry's name
+   */
+  public String getName() {
+    return NetworkTablesJNI.getEntryName(m_handle);
+  }
+
+  /**
+   * Gets the type of the entry.
+   *
+   * @return the entry's type
+   */
+  public NetworkTableType getType() {
+    return NetworkTableType.getFromInt(NetworkTablesJNI.getType(m_handle));
+  }
+
+  /**
+   * Returns the flags.
+   *
+   * @return the flags (bitmask)
+   * @deprecated Use isPersistent() or topic properties instead
+   */
+  @Deprecated(since = "2022", forRemoval = true)
+  public int getFlags() {
+    return NetworkTablesJNI.getEntryFlags(m_handle);
+  }
+
+  /**
+   * Gets the last time the entry's value was changed.
+   *
+   * @return Entry last change time
+   */
+  @Override
+  public long getLastChange() {
+    return NetworkTablesJNI.getEntryLastChange(m_handle);
+  }
+
+  /**
+   * Gets the entry's value. Returns a value with type NetworkTableType.kUnassigned if the value
+   * does not exist.
+   *
+   * @return the entry's value
+   */
+  public NetworkTableValue getValue() {
+    return NetworkTablesJNI.getValue(m_handle);
+  }
+
+  /**
+   * Gets the entry's value as a boolean. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public boolean getBoolean(boolean defaultValue) {
+    return NetworkTablesJNI.getBoolean(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a long. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public long getInteger(long defaultValue) {
+    return NetworkTablesJNI.getInteger(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a float. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public float getFloat(float defaultValue) {
+    return NetworkTablesJNI.getFloat(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a double. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public double getDouble(double defaultValue) {
+    return NetworkTablesJNI.getDouble(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a String. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public String getString(String defaultValue) {
+    return NetworkTablesJNI.getString(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a byte[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public byte[] getRaw(byte[] defaultValue) {
+    return NetworkTablesJNI.getRaw(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public boolean[] getBooleanArray(boolean[] defaultValue) {
+    return NetworkTablesJNI.getBooleanArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Boolean[] getBooleanArray(Boolean[] defaultValue) {
+    return NetworkTableValue.fromNativeBooleanArray(
+        getBooleanArray(NetworkTableValue.toNativeBooleanArray(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a long[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public long[] getIntegerArray(long[] defaultValue) {
+    return NetworkTablesJNI.getIntegerArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Long[] getIntegerArray(Long[] defaultValue) {
+    return NetworkTableValue.fromNativeIntegerArray(
+        getIntegerArray(NetworkTableValue.toNativeIntegerArray(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a float[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public float[] getFloatArray(float[] defaultValue) {
+    return NetworkTablesJNI.getFloatArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Float[] getFloatArray(Float[] defaultValue) {
+    return NetworkTableValue.fromNativeFloatArray(
+        getFloatArray(NetworkTableValue.toNativeFloatArray(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a double[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public double[] getDoubleArray(double[] defaultValue) {
+    return NetworkTablesJNI.getDoubleArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Double[] getDoubleArray(Double[] defaultValue) {
+    return NetworkTableValue.fromNativeDoubleArray(
+        getDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a String[]. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public String[] getStringArray(String[] defaultValue) {
+    return NetworkTablesJNI.getStringArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a double. If the entry does not exist or is of different type, it
+   * will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Number getNumber(Number defaultValue) {
+    return getDouble(defaultValue.doubleValue());
+  }
+
+  /**
+   * Gets the entry's value as a double array. If the entry does not exist or is of different type,
+   * it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Number[] getNumberArray(Number[] defaultValue) {
+    return NetworkTableValue.fromNativeDoubleArray(
+        getDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue)));
+  }
+
+  /**
+   * Get an array of all value changes since the last call to readQueue.
+   *
+   * <p>The "poll storage" subscribe option can be used to set the queue
+   * depth.
+   *
+   * @return Array of values; empty array if no new changes have been
+   *     published since the previous call.
+   */
+  public NetworkTableValue[] readQueue() {
+    return NetworkTablesJNI.readQueueValue(m_handle);
+  }
+
+  /**
+   * Checks if a data value is of a type that can be placed in a NetworkTable entry.
+   *
+   * @param data the data to check
+   * @return true if the data can be placed in an entry, false if it cannot
+   */
+  public static boolean isValidDataType(Object data) {
+    return data instanceof Number
+        || data instanceof Boolean
+        || data instanceof String
+        || data instanceof long[]
+        || data instanceof Long[]
+        || data instanceof float[]
+        || data instanceof Float[]
+        || data instanceof double[]
+        || data instanceof Double[]
+        || data instanceof Number[]
+        || data instanceof boolean[]
+        || data instanceof Boolean[]
+        || data instanceof String[]
+        || data instanceof byte[]
+        || data instanceof Byte[];
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  public boolean setDefaultValue(Object defaultValue) {
+    if (defaultValue instanceof NetworkTableValue) {
+      long time = ((NetworkTableValue) defaultValue).getTime();
+      Object otherValue = ((NetworkTableValue) defaultValue).getValue();
+      switch (((NetworkTableValue) defaultValue).getType()) {
+        case kBoolean:
+          return NetworkTablesJNI.setDefaultBoolean(m_handle, time, (Boolean) otherValue);
+        case kInteger:
+          return NetworkTablesJNI.setDefaultInteger(
+              m_handle, time, ((Number) otherValue).longValue());
+        case kFloat:
+          return NetworkTablesJNI.setDefaultFloat(
+              m_handle, time, ((Number) otherValue).floatValue());
+        case kDouble:
+          return NetworkTablesJNI.setDefaultDouble(
+              m_handle, time, ((Number) otherValue).doubleValue());
+        case kString:
+          return NetworkTablesJNI.setDefaultString(m_handle, time, (String) otherValue);
+        case kRaw:
+          return NetworkTablesJNI.setDefaultRaw(m_handle, time, (byte[]) otherValue);
+        case kBooleanArray:
+          return NetworkTablesJNI.setDefaultBooleanArray(m_handle, time, (boolean[]) otherValue);
+        case kIntegerArray:
+          return NetworkTablesJNI.setDefaultIntegerArray(m_handle, time, (long[]) otherValue);
+        case kFloatArray:
+          return NetworkTablesJNI.setDefaultFloatArray(m_handle, time, (float[]) otherValue);
+        case kDoubleArray:
+          return NetworkTablesJNI.setDefaultDoubleArray(m_handle, time, (double[]) otherValue);
+        case kStringArray:
+          return NetworkTablesJNI.setDefaultStringArray(m_handle, time, (String[]) otherValue);
+        default:
+          return true;
+      }
+    } else if (defaultValue instanceof Boolean) {
+      return setDefaultBoolean((Boolean) defaultValue);
+    } else if (defaultValue instanceof Integer) {
+      return setDefaultInteger((Integer) defaultValue);
+    } else if (defaultValue instanceof Float) {
+      return setDefaultFloat((Float) defaultValue);
+    } else if (defaultValue instanceof Number) {
+      return setDefaultNumber((Number) defaultValue);
+    } else if (defaultValue instanceof String) {
+      return setDefaultString((String) defaultValue);
+    } else if (defaultValue instanceof byte[]) {
+      return setDefaultRaw((byte[]) defaultValue);
+    } else if (defaultValue instanceof boolean[]) {
+      return setDefaultBooleanArray((boolean[]) defaultValue);
+    } else if (defaultValue instanceof long[]) {
+      return setDefaultIntegerArray((long[]) defaultValue);
+    } else if (defaultValue instanceof float[]) {
+      return setDefaultFloatArray((float[]) defaultValue);
+    } else if (defaultValue instanceof double[]) {
+      return setDefaultDoubleArray((double[]) defaultValue);
+    } else if (defaultValue instanceof Boolean[]) {
+      return setDefaultBooleanArray((Boolean[]) defaultValue);
+    } else if (defaultValue instanceof Long[]) {
+      return setDefaultIntegerArray((Long[]) defaultValue);
+    } else if (defaultValue instanceof Float[]) {
+      return setDefaultFloatArray((Float[]) defaultValue);
+    } else if (defaultValue instanceof Number[]) {
+      return setDefaultNumberArray((Number[]) defaultValue);
+    } else if (defaultValue instanceof String[]) {
+      return setDefaultStringArray((String[]) defaultValue);
+    } else {
+      throw new IllegalArgumentException(
+          "Value of type " + defaultValue.getClass().getName() + " cannot be put into a table");
+    }
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultBoolean(boolean defaultValue) {
+    return NetworkTablesJNI.setDefaultBoolean(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultInteger(long defaultValue) {
+    return NetworkTablesJNI.setDefaultInteger(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultFloat(float defaultValue) {
+    return NetworkTablesJNI.setDefaultFloat(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultDouble(double defaultValue) {
+    return NetworkTablesJNI.setDefaultDouble(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultString(String defaultValue) {
+    return NetworkTablesJNI.setDefaultString(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultRaw(byte[] defaultValue) {
+    return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set; will send from defaultValue.position() to
+   *                     defaultValue.capacity()
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultRaw(ByteBuffer defaultValue) {
+    return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.length - start)
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultRaw(byte[] defaultValue, int start, int len) {
+    return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue, start, len);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.capacity() - start)
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultRaw(ByteBuffer defaultValue, int start, int len) {
+    return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue, start, len);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultBooleanArray(boolean[] defaultValue) {
+    return NetworkTablesJNI.setDefaultBooleanArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultBooleanArray(Boolean[] defaultValue) {
+    return setDefaultBooleanArray(NetworkTableValue.toNativeBooleanArray(defaultValue));
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultIntegerArray(long[] defaultValue) {
+    return NetworkTablesJNI.setDefaultIntegerArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultIntegerArray(Long[] defaultValue) {
+    return setDefaultIntegerArray(NetworkTableValue.toNativeIntegerArray(defaultValue));
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultFloatArray(float[] defaultValue) {
+    return NetworkTablesJNI.setDefaultFloatArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultFloatArray(Float[] defaultValue) {
+    return setDefaultFloatArray(NetworkTableValue.toNativeFloatArray(defaultValue));
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultDoubleArray(double[] defaultValue) {
+    return NetworkTablesJNI.setDefaultDoubleArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultDoubleArray(Double[] defaultValue) {
+    return setDefaultDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue));
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultStringArray(String[] defaultValue) {
+    return NetworkTablesJNI.setDefaultStringArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultNumber(Number defaultValue) {
+    return setDefaultDouble(defaultValue.doubleValue());
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultNumberArray(Number[] defaultValue) {
+    return setDefaultDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue));
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  public boolean setValue(Object value) {
+    if (value instanceof NetworkTableValue) {
+      long time = ((NetworkTableValue) value).getTime();
+      Object otherValue = ((NetworkTableValue) value).getValue();
+      switch (((NetworkTableValue) value).getType()) {
+        case kBoolean:
+          return NetworkTablesJNI.setBoolean(m_handle, time, (Boolean) otherValue);
+        case kInteger:
+          return NetworkTablesJNI.setInteger(
+              m_handle, time, ((Number) otherValue).longValue());
+        case kFloat:
+          return NetworkTablesJNI.setFloat(
+              m_handle, time, ((Number) otherValue).floatValue());
+        case kDouble:
+          return NetworkTablesJNI.setDouble(
+              m_handle, time, ((Number) otherValue).doubleValue());
+        case kString:
+          return NetworkTablesJNI.setString(m_handle, time, (String) otherValue);
+        case kRaw:
+          return NetworkTablesJNI.setRaw(m_handle, time, (byte[]) otherValue);
+        case kBooleanArray:
+          return NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[]) otherValue);
+        case kIntegerArray:
+          return NetworkTablesJNI.setIntegerArray(m_handle, time, (long[]) otherValue);
+        case kFloatArray:
+          return NetworkTablesJNI.setFloatArray(m_handle, time, (float[]) otherValue);
+        case kDoubleArray:
+          return NetworkTablesJNI.setDoubleArray(m_handle, time, (double[]) otherValue);
+        case kStringArray:
+          return NetworkTablesJNI.setStringArray(m_handle, time, (String[]) otherValue);
+        default:
+          return true;
+      }
+    } else if (value instanceof Boolean) {
+      return setBoolean((Boolean) value);
+    } else if (value instanceof Long) {
+      return setInteger((Long) value);
+    } else if (value instanceof Float) {
+      return setFloat((Float) value);
+    } else if (value instanceof Number) {
+      return setNumber((Number) value);
+    } else if (value instanceof String) {
+      return setString((String) value);
+    } else if (value instanceof byte[]) {
+      return setRaw((byte[]) value);
+    } else if (value instanceof boolean[]) {
+      return setBooleanArray((boolean[]) value);
+    } else if (value instanceof long[]) {
+      return setIntegerArray((long[]) value);
+    } else if (value instanceof float[]) {
+      return setFloatArray((float[]) value);
+    } else if (value instanceof double[]) {
+      return setDoubleArray((double[]) value);
+    } else if (value instanceof Boolean[]) {
+      return setBooleanArray((Boolean[]) value);
+    } else if (value instanceof Long[]) {
+      return setIntegerArray((Long[]) value);
+    } else if (value instanceof Float[]) {
+      return setFloatArray((Float[]) value);
+    } else if (value instanceof Number[]) {
+      return setNumberArray((Number[]) value);
+    } else if (value instanceof String[]) {
+      return setStringArray((String[]) value);
+    } else {
+      throw new IllegalArgumentException(
+          "Value of type " + value.getClass().getName() + " cannot be put into a table");
+    }
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setBoolean(boolean value) {
+    return NetworkTablesJNI.setBoolean(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setInteger(long value) {
+    return NetworkTablesJNI.setInteger(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setFloat(float value) {
+    return NetworkTablesJNI.setFloat(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDouble(double value) {
+    return NetworkTablesJNI.setDouble(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setString(String value) {
+    return NetworkTablesJNI.setString(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setRaw(byte[] value) {
+    return NetworkTablesJNI.setRaw(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set; will send from value.position() to value.capacity()
+   * @return False if the entry exists with a different type
+   */
+  public boolean setRaw(ByteBuffer value) {
+    return NetworkTablesJNI.setRaw(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.length - start)
+   * @return False if the entry exists with a different type
+   */
+  public boolean setRaw(byte[] value, int start, int len) {
+    return NetworkTablesJNI.setRaw(m_handle, 0, value, start, len);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @param start Start position of data (in buffer)
+   * @param len Length of data (must be less than or equal to value.capacity() - start)
+   * @return False if the entry exists with a different type
+   */
+  public boolean setRaw(ByteBuffer value, int start, int len) {
+    return NetworkTablesJNI.setRaw(m_handle, 0, value, start, len);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setBooleanArray(boolean[] value) {
+    return NetworkTablesJNI.setBooleanArray(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setBooleanArray(Boolean[] value) {
+    return setBooleanArray(NetworkTableValue.toNativeBooleanArray(value));
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setIntegerArray(long[] value) {
+    return NetworkTablesJNI.setIntegerArray(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setIntegerArray(Long[] value) {
+    return setIntegerArray(NetworkTableValue.toNativeIntegerArray(value));
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setFloatArray(float[] value) {
+    return NetworkTablesJNI.setFloatArray(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setFloatArray(Float[] value) {
+    return setFloatArray(NetworkTableValue.toNativeFloatArray(value));
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDoubleArray(double[] value) {
+    return NetworkTablesJNI.setDoubleArray(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDoubleArray(Double[] value) {
+    return setDoubleArray(NetworkTableValue.toNativeDoubleArray(value));
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setStringArray(String[] value) {
+    return NetworkTablesJNI.setStringArray(m_handle, 0, value);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setNumber(Number value) {
+    return setDouble(value.doubleValue());
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setNumberArray(Number[] value) {
+    return setDoubleArray(NetworkTableValue.toNativeDoubleArray(value));
+  }
+
+  /**
+   * Sets flags.
+   *
+   * @param flags the flags to set (bitmask)
+   * @deprecated Use setPersistent() or topic properties instead
+   */
+  @Deprecated(since = "2022", forRemoval = true)
+  public void setFlags(int flags) {
+    NetworkTablesJNI.setEntryFlags(m_handle, getFlags() | flags);
+  }
+
+  /**
+   * Clears flags.
+   *
+   * @param flags the flags to clear (bitmask)
+   * @deprecated Use setPersistent() or topic properties instead
+   */
+  @Deprecated(since = "2022", forRemoval = true)
+  public void clearFlags(int flags) {
+    NetworkTablesJNI.setEntryFlags(m_handle, getFlags() & ~flags);
+  }
+
+  /** Make value persistent through program restarts. */
+  public void setPersistent() {
+    NetworkTablesJNI.setTopicPersistent(m_topic.getHandle(), true);
+  }
+
+  /** Stop making value persistent through program restarts. */
+  public void clearPersistent() {
+    NetworkTablesJNI.setTopicPersistent(m_topic.getHandle(), false);
+  }
+
+  /**
+   * Returns whether the value is persistent through program restarts.
+   *
+   * @return True if the value is persistent.
+   */
+  public boolean isPersistent() {
+    return NetworkTablesJNI.getTopicPersistent(m_topic.getHandle());
+  }
+
+  /** Stops publishing the entry if it's been published. */
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  /**
+   * Deletes the entry.
+   *
+   * @deprecated Use unpublish() instead.
+   */
+  @Deprecated(since = "2022", forRemoval = true)
+  public void delete() {
+    unpublish();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == this) {
+      return true;
+    }
+    if (!(other instanceof NetworkTableEntry)) {
+      return false;
+    }
+
+    return m_handle == ((NetworkTableEntry) other).m_handle;
+  }
+
+  @Override
+  public int hashCode() {
+    return m_handle;
+  }
+
+  private final Topic m_topic;
+  protected int m_handle;
+}
diff --git a/ntcore/src/generate/java/NetworkTableInstance.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableInstance.java
similarity index 85%
copy from ntcore/src/generate/java/NetworkTableInstance.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableInstance.java
index 9df129d..e5872ba 100644
--- a/ntcore/src/generate/java/NetworkTableInstance.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableInstance.java
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import edu.wpi.first.util.WPIUtilJNI;
@@ -163,17 +165,17 @@
     }
     return topic;
   }
-{% for t in types %}
+
   /**
-   * Get {{ t.java.ValueType }} topic.
+   * Get boolean topic.
    *
    * @param name topic name
-   * @return {{ t.TypeName }}Topic
+   * @return BooleanTopic
    */
-  public {{ t.TypeName }}Topic get{{ t.TypeName }}Topic(String name) {
+  public BooleanTopic getBooleanTopic(String name) {
     Topic topic = m_topics.get(name);
-    if (topic instanceof {{ t.TypeName }}Topic) {
-      return ({{ t.TypeName }}Topic) topic;
+    if (topic instanceof BooleanTopic) {
+      return (BooleanTopic) topic;
     }
 
     int handle;
@@ -183,7 +185,7 @@
       handle = topic.getHandle();
     }
 
-    {{ t.TypeName }}Topic wrapTopic = new {{ t.TypeName }}Topic(this, handle);
+    BooleanTopic wrapTopic = new BooleanTopic(this, handle);
     m_topics.put(name, wrapTopic);
 
     // also cache by handle
@@ -191,7 +193,287 @@
 
     return wrapTopic;
   }
-{% endfor %}
+
+  /**
+   * Get long topic.
+   *
+   * @param name topic name
+   * @return IntegerTopic
+   */
+  public IntegerTopic getIntegerTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof IntegerTopic) {
+      return (IntegerTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    IntegerTopic wrapTopic = new IntegerTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get float topic.
+   *
+   * @param name topic name
+   * @return FloatTopic
+   */
+  public FloatTopic getFloatTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof FloatTopic) {
+      return (FloatTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    FloatTopic wrapTopic = new FloatTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get double topic.
+   *
+   * @param name topic name
+   * @return DoubleTopic
+   */
+  public DoubleTopic getDoubleTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof DoubleTopic) {
+      return (DoubleTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    DoubleTopic wrapTopic = new DoubleTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get String topic.
+   *
+   * @param name topic name
+   * @return StringTopic
+   */
+  public StringTopic getStringTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof StringTopic) {
+      return (StringTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    StringTopic wrapTopic = new StringTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get byte[] topic.
+   *
+   * @param name topic name
+   * @return RawTopic
+   */
+  public RawTopic getRawTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof RawTopic) {
+      return (RawTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    RawTopic wrapTopic = new RawTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get boolean[] topic.
+   *
+   * @param name topic name
+   * @return BooleanArrayTopic
+   */
+  public BooleanArrayTopic getBooleanArrayTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof BooleanArrayTopic) {
+      return (BooleanArrayTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    BooleanArrayTopic wrapTopic = new BooleanArrayTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get long[] topic.
+   *
+   * @param name topic name
+   * @return IntegerArrayTopic
+   */
+  public IntegerArrayTopic getIntegerArrayTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof IntegerArrayTopic) {
+      return (IntegerArrayTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    IntegerArrayTopic wrapTopic = new IntegerArrayTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get float[] topic.
+   *
+   * @param name topic name
+   * @return FloatArrayTopic
+   */
+  public FloatArrayTopic getFloatArrayTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof FloatArrayTopic) {
+      return (FloatArrayTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    FloatArrayTopic wrapTopic = new FloatArrayTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get double[] topic.
+   *
+   * @param name topic name
+   * @return DoubleArrayTopic
+   */
+  public DoubleArrayTopic getDoubleArrayTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof DoubleArrayTopic) {
+      return (DoubleArrayTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    DoubleArrayTopic wrapTopic = new DoubleArrayTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
+  /**
+   * Get String[] topic.
+   *
+   * @param name topic name
+   * @return StringArrayTopic
+   */
+  public StringArrayTopic getStringArrayTopic(String name) {
+    Topic topic = m_topics.get(name);
+    if (topic instanceof StringArrayTopic) {
+      return (StringArrayTopic) topic;
+    }
+
+    int handle;
+    if (topic == null) {
+      handle = NetworkTablesJNI.getTopic(m_handle, name);
+    } else {
+      handle = topic.getHandle();
+    }
+
+    StringArrayTopic wrapTopic = new StringArrayTopic(this, handle);
+    m_topics.put(name, wrapTopic);
+
+    // also cache by handle
+    m_topicsByHandle.put(handle, wrapTopic);
+
+    return wrapTopic;
+  }
+
 
   /**
    * Get protobuf-encoded value topic.
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableValue.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableValue.java
new file mode 100644
index 0000000..3045d28
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTableValue.java
@@ -0,0 +1,742 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.Objects;
+
+/** A network table entry value. */
+@SuppressWarnings({"UnnecessaryParentheses", "PMD.MethodReturnsInternalArray"})
+public final class NetworkTableValue {
+  NetworkTableValue(NetworkTableType type, Object value, long time, long serverTime) {
+    m_type = type;
+    m_value = value;
+    m_time = time;
+    m_serverTime = serverTime;
+  }
+
+  NetworkTableValue(NetworkTableType type, Object value, long time) {
+    this(type, value, time, time == 0 ? 0 : 1);
+  }
+
+  NetworkTableValue(NetworkTableType type, Object value) {
+    this(type, value, NetworkTablesJNI.now(), 1);
+  }
+
+  NetworkTableValue(int type, Object value, long time, long serverTime) {
+    this(NetworkTableType.getFromInt(type), value, time, serverTime);
+  }
+
+  /**
+   * Get the data type.
+   *
+   * @return The type.
+   */
+  public NetworkTableType getType() {
+    return m_type;
+  }
+
+  /**
+   * Get the data value stored.
+   *
+   * @return The type.
+   */
+  public Object getValue() {
+    return m_value;
+  }
+
+  /**
+   * Get the creation time of the value in local time.
+   *
+   * @return The time, in the units returned by NetworkTablesJNI.now().
+   */
+  public long getTime() {
+    return m_time;
+  }
+
+  /**
+   * Get the creation time of the value in server time.
+   *
+   * @return The server time.
+   */
+  public long getServerTime() {
+    return m_serverTime;
+  }
+
+  /*
+   * Type Checkers
+   */
+
+  /**
+   * Determine if entry value contains a value or is unassigned.
+   *
+   * @return True if the entry value contains a value.
+   */
+  public boolean isValid() {
+    return m_type != NetworkTableType.kUnassigned;
+  }
+
+  /**
+   * Determine if entry value contains a boolean.
+   *
+   * @return True if the entry value is of boolean type.
+   */
+  public boolean isBoolean() {
+    return m_type == NetworkTableType.kBoolean;
+  }
+
+  /**
+   * Determine if entry value contains a long.
+   *
+   * @return True if the entry value is of long type.
+   */
+  public boolean isInteger() {
+    return m_type == NetworkTableType.kInteger;
+  }
+
+  /**
+   * Determine if entry value contains a float.
+   *
+   * @return True if the entry value is of float type.
+   */
+  public boolean isFloat() {
+    return m_type == NetworkTableType.kFloat;
+  }
+
+  /**
+   * Determine if entry value contains a double.
+   *
+   * @return True if the entry value is of double type.
+   */
+  public boolean isDouble() {
+    return m_type == NetworkTableType.kDouble;
+  }
+
+  /**
+   * Determine if entry value contains a String.
+   *
+   * @return True if the entry value is of String type.
+   */
+  public boolean isString() {
+    return m_type == NetworkTableType.kString;
+  }
+
+  /**
+   * Determine if entry value contains a byte[].
+   *
+   * @return True if the entry value is of byte[] type.
+   */
+  public boolean isRaw() {
+    return m_type == NetworkTableType.kRaw;
+  }
+
+  /**
+   * Determine if entry value contains a boolean[].
+   *
+   * @return True if the entry value is of boolean[] type.
+   */
+  public boolean isBooleanArray() {
+    return m_type == NetworkTableType.kBooleanArray;
+  }
+
+  /**
+   * Determine if entry value contains a long[].
+   *
+   * @return True if the entry value is of long[] type.
+   */
+  public boolean isIntegerArray() {
+    return m_type == NetworkTableType.kIntegerArray;
+  }
+
+  /**
+   * Determine if entry value contains a float[].
+   *
+   * @return True if the entry value is of float[] type.
+   */
+  public boolean isFloatArray() {
+    return m_type == NetworkTableType.kFloatArray;
+  }
+
+  /**
+   * Determine if entry value contains a double[].
+   *
+   * @return True if the entry value is of double[] type.
+   */
+  public boolean isDoubleArray() {
+    return m_type == NetworkTableType.kDoubleArray;
+  }
+
+  /**
+   * Determine if entry value contains a String[].
+   *
+   * @return True if the entry value is of String[] type.
+   */
+  public boolean isStringArray() {
+    return m_type == NetworkTableType.kStringArray;
+  }
+
+  /*
+   * Type-Safe Getters
+   */
+
+  /**
+   * Get the boolean value.
+   *
+   * @return The boolean value.
+   * @throws ClassCastException if the entry value is not of boolean type.
+   */
+  public boolean getBoolean() {
+    if (m_type != NetworkTableType.kBoolean) {
+      throw new ClassCastException("cannot convert " + m_type + " to boolean");
+    }
+    return (Boolean) m_value;
+  }
+
+  /**
+   * Get the long value.
+   *
+   * @return The long value.
+   * @throws ClassCastException if the entry value is not of long type.
+   */
+  public long getInteger() {
+    if (m_type != NetworkTableType.kInteger) {
+      throw new ClassCastException("cannot convert " + m_type + " to long");
+    }
+    return ((Number) m_value).longValue();
+  }
+
+  /**
+   * Get the float value.
+   *
+   * @return The float value.
+   * @throws ClassCastException if the entry value is not of float type.
+   */
+  public float getFloat() {
+    if (m_type != NetworkTableType.kFloat) {
+      throw new ClassCastException("cannot convert " + m_type + " to float");
+    }
+    return ((Number) m_value).floatValue();
+  }
+
+  /**
+   * Get the double value.
+   *
+   * @return The double value.
+   * @throws ClassCastException if the entry value is not of double type.
+   */
+  public double getDouble() {
+    if (m_type != NetworkTableType.kDouble) {
+      throw new ClassCastException("cannot convert " + m_type + " to double");
+    }
+    return ((Number) m_value).doubleValue();
+  }
+
+  /**
+   * Get the String value.
+   *
+   * @return The String value.
+   * @throws ClassCastException if the entry value is not of String type.
+   */
+  public String getString() {
+    if (m_type != NetworkTableType.kString) {
+      throw new ClassCastException("cannot convert " + m_type + " to String");
+    }
+    return (String) m_value;
+  }
+
+  /**
+   * Get the byte[] value.
+   *
+   * @return The byte[] value.
+   * @throws ClassCastException if the entry value is not of byte[] type.
+   */
+  public byte[] getRaw() {
+    if (m_type != NetworkTableType.kRaw) {
+      throw new ClassCastException("cannot convert " + m_type + " to byte[]");
+    }
+    return (byte[]) m_value;
+  }
+
+  /**
+   * Get the boolean[] value.
+   *
+   * @return The boolean[] value.
+   * @throws ClassCastException if the entry value is not of boolean[] type.
+   */
+  public boolean[] getBooleanArray() {
+    if (m_type != NetworkTableType.kBooleanArray) {
+      throw new ClassCastException("cannot convert " + m_type + " to boolean[]");
+    }
+    return (boolean[]) m_value;
+  }
+
+  /**
+   * Get the long[] value.
+   *
+   * @return The long[] value.
+   * @throws ClassCastException if the entry value is not of long[] type.
+   */
+  public long[] getIntegerArray() {
+    if (m_type != NetworkTableType.kIntegerArray) {
+      throw new ClassCastException("cannot convert " + m_type + " to long[]");
+    }
+    return (long[]) m_value;
+  }
+
+  /**
+   * Get the float[] value.
+   *
+   * @return The float[] value.
+   * @throws ClassCastException if the entry value is not of float[] type.
+   */
+  public float[] getFloatArray() {
+    if (m_type != NetworkTableType.kFloatArray) {
+      throw new ClassCastException("cannot convert " + m_type + " to float[]");
+    }
+    return (float[]) m_value;
+  }
+
+  /**
+   * Get the double[] value.
+   *
+   * @return The double[] value.
+   * @throws ClassCastException if the entry value is not of double[] type.
+   */
+  public double[] getDoubleArray() {
+    if (m_type != NetworkTableType.kDoubleArray) {
+      throw new ClassCastException("cannot convert " + m_type + " to double[]");
+    }
+    return (double[]) m_value;
+  }
+
+  /**
+   * Get the String[] value.
+   *
+   * @return The String[] value.
+   * @throws ClassCastException if the entry value is not of String[] type.
+   */
+  public String[] getStringArray() {
+    if (m_type != NetworkTableType.kStringArray) {
+      throw new ClassCastException("cannot convert " + m_type + " to String[]");
+    }
+    return (String[]) m_value;
+  }
+
+  /*
+   * Factory functions.
+   */
+
+  /**
+   * Creates a boolean value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBoolean(boolean value) {
+    return new NetworkTableValue(NetworkTableType.kBoolean, Boolean.valueOf(value));
+  }
+
+  /**
+   * Creates a boolean value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBoolean(boolean value, long time) {
+    return new NetworkTableValue(NetworkTableType.kBoolean, Boolean.valueOf(value), time);
+  }
+
+  /**
+   * Creates a long value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeInteger(long value) {
+    return new NetworkTableValue(NetworkTableType.kInteger, Long.valueOf(value));
+  }
+
+  /**
+   * Creates a long value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeInteger(long value, long time) {
+    return new NetworkTableValue(NetworkTableType.kInteger, Long.valueOf(value), time);
+  }
+
+  /**
+   * Creates a float value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeFloat(float value) {
+    return new NetworkTableValue(NetworkTableType.kFloat, Float.valueOf(value));
+  }
+
+  /**
+   * Creates a float value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeFloat(float value, long time) {
+    return new NetworkTableValue(NetworkTableType.kFloat, Float.valueOf(value), time);
+  }
+
+  /**
+   * Creates a double value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDouble(double value) {
+    return new NetworkTableValue(NetworkTableType.kDouble, Double.valueOf(value));
+  }
+
+  /**
+   * Creates a double value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDouble(double value, long time) {
+    return new NetworkTableValue(NetworkTableType.kDouble, Double.valueOf(value), time);
+  }
+
+  /**
+   * Creates a String value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeString(String value) {
+    return new NetworkTableValue(NetworkTableType.kString, (value));
+  }
+
+  /**
+   * Creates a String value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeString(String value, long time) {
+    return new NetworkTableValue(NetworkTableType.kString, (value), time);
+  }
+
+  /**
+   * Creates a byte[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeRaw(byte[] value) {
+    return new NetworkTableValue(NetworkTableType.kRaw, (value));
+  }
+
+  /**
+   * Creates a byte[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeRaw(byte[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kRaw, (value), time);
+  }
+
+  /**
+   * Creates a boolean[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBooleanArray(boolean[] value) {
+    return new NetworkTableValue(NetworkTableType.kBooleanArray, (value));
+  }
+
+  /**
+   * Creates a boolean[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBooleanArray(boolean[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kBooleanArray, (value), time);
+  }
+
+  /**
+   * Creates a boolean[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBooleanArray(Boolean[] value) {
+    return new NetworkTableValue(NetworkTableType.kBooleanArray, toNativeBooleanArray(value));
+  }
+
+  /**
+   * Creates a boolean[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBooleanArray(Boolean[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kBooleanArray, toNativeBooleanArray(value), time);
+  }
+
+  /**
+   * Creates a long[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeIntegerArray(long[] value) {
+    return new NetworkTableValue(NetworkTableType.kIntegerArray, (value));
+  }
+
+  /**
+   * Creates a long[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeIntegerArray(long[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kIntegerArray, (value), time);
+  }
+
+  /**
+   * Creates a long[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeIntegerArray(Long[] value) {
+    return new NetworkTableValue(NetworkTableType.kIntegerArray, toNativeIntegerArray(value));
+  }
+
+  /**
+   * Creates a long[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeIntegerArray(Long[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kIntegerArray, toNativeIntegerArray(value), time);
+  }
+
+  /**
+   * Creates a float[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeFloatArray(float[] value) {
+    return new NetworkTableValue(NetworkTableType.kFloatArray, (value));
+  }
+
+  /**
+   * Creates a float[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeFloatArray(float[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kFloatArray, (value), time);
+  }
+
+  /**
+   * Creates a float[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeFloatArray(Float[] value) {
+    return new NetworkTableValue(NetworkTableType.kFloatArray, toNativeFloatArray(value));
+  }
+
+  /**
+   * Creates a float[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeFloatArray(Float[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kFloatArray, toNativeFloatArray(value), time);
+  }
+
+  /**
+   * Creates a double[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDoubleArray(double[] value) {
+    return new NetworkTableValue(NetworkTableType.kDoubleArray, (value));
+  }
+
+  /**
+   * Creates a double[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDoubleArray(double[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kDoubleArray, (value), time);
+  }
+
+  /**
+   * Creates a double[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDoubleArray(Double[] value) {
+    return new NetworkTableValue(NetworkTableType.kDoubleArray, toNativeDoubleArray(value));
+  }
+
+  /**
+   * Creates a double[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDoubleArray(Double[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kDoubleArray, toNativeDoubleArray(value), time);
+  }
+
+  /**
+   * Creates a String[] value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeStringArray(String[] value) {
+    return new NetworkTableValue(NetworkTableType.kStringArray, (value));
+  }
+
+  /**
+   * Creates a String[] value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeStringArray(String[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kStringArray, (value), time);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == this) {
+      return true;
+    }
+    if (!(other instanceof NetworkTableValue)) {
+      return false;
+    }
+    NetworkTableValue ntOther = (NetworkTableValue) other;
+    return m_type == ntOther.m_type && m_value.equals(ntOther.m_value);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(m_type, m_value);
+  }
+
+  // arraycopy() doesn't know how to unwrap boxed values; this is a false positive in PMD
+  // (see https://sourceforge.net/p/pmd/bugs/804/)
+  @SuppressWarnings("PMD.AvoidArrayLoops")
+  static boolean[] toNativeBooleanArray(Boolean[] arr) {
+    boolean[] out = new boolean[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i];
+    }
+    return out;
+  }
+
+  @SuppressWarnings("PMD.AvoidArrayLoops")
+  static double[] toNativeDoubleArray(Number[] arr) {
+    double[] out = new double[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i].doubleValue();
+    }
+    return out;
+  }
+
+  @SuppressWarnings("PMD.AvoidArrayLoops")
+  static long[] toNativeIntegerArray(Number[] arr) {
+    long[] out = new long[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i].longValue();
+    }
+    return out;
+  }
+
+  @SuppressWarnings("PMD.AvoidArrayLoops")
+  static float[] toNativeFloatArray(Number[] arr) {
+    float[] out = new float[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i].floatValue();
+    }
+    return out;
+  }
+
+  @SuppressWarnings("PMD.AvoidArrayLoops")
+  static Boolean[] fromNativeBooleanArray(boolean[] arr) {
+    Boolean[] out = new Boolean[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i];
+    }
+    return out;
+  }
+
+  @SuppressWarnings("PMD.AvoidArrayLoops")
+  static Long[] fromNativeIntegerArray(long[] arr) {
+    Long[] out = new Long[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i];
+    }
+    return out;
+  }
+
+  @SuppressWarnings("PMD.AvoidArrayLoops")
+  static Float[] fromNativeFloatArray(float[] arr) {
+    Float[] out = new Float[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i];
+    }
+    return out;
+  }
+
+  @SuppressWarnings("PMD.AvoidArrayLoops")
+  static Double[] fromNativeDoubleArray(double[] arr) {
+    Double[] out = new Double[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i];
+    }
+    return out;
+  }
+
+  private NetworkTableType m_type;
+  private Object m_value;
+  private long m_time;
+  private long m_serverTime;
+}
diff --git a/ntcore/src/generate/java/NetworkTablesJNI.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java
similarity index 64%
copy from ntcore/src/generate/java/NetworkTablesJNI.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java
index 6ff9c16..5aa4060 100644
--- a/ntcore/src/generate/java/NetworkTablesJNI.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java
@@ -2,6 +2,8 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 import edu.wpi.first.util.RuntimeLoader;
@@ -16,16 +18,30 @@
   static boolean libraryLoaded = false;
   static RuntimeLoader<NetworkTablesJNI> loader = null;
 
+  /** Sets whether JNI should be loaded in the static block. */
   public static class Helper {
     private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
 
+    /**
+     * Returns true if the JNI should be loaded in the static block.
+     *
+     * @return True if the JNI should be loaded in the static block.
+     */
     public static boolean getExtractOnStaticLoad() {
       return extractOnStaticLoad.get();
     }
 
+    /**
+     * Sets whether the JNI should be loaded in the static block.
+     *
+     * @param load Whether the JNI should be loaded in the static block.
+     */
     public static void setExtractOnStaticLoad(boolean load) {
       extractOnStaticLoad.set(load);
     }
+
+    /** Utility class. */
+    private Helper() {}
   }
 
   static {
@@ -121,6 +137,10 @@
 
   public static native boolean getTopicRetained(int topic);
 
+  public static native void setTopicCached(int topic, boolean value);
+
+  public static native boolean getTopicCached(int topic);
+
   public static native String getTopicTypeString(int topic);
 
   public static native boolean getTopicExists(int topic);
@@ -176,14 +196,84 @@
   }
 
   public static native void unsubscribeMultiple(int sub);
-{% for t in types %}
-  public static native Timestamped{{ t.TypeName }} getAtomic{{ t.TypeName }}(
-      int subentry, {{ t.java.ValueType }} defaultValue);
 
-  public static native Timestamped{{ t.TypeName }}[] readQueue{{ t.TypeName }}(int subentry);
+  public static native TimestampedBoolean getAtomicBoolean(
+      int subentry, boolean defaultValue);
 
-  public static native {{ t.java.ValueType }}[] readQueueValues{{ t.TypeName }}(int subentry);
-{% if t.TypeName == "Raw" %}
+  public static native TimestampedBoolean[] readQueueBoolean(int subentry);
+
+  public static native boolean[] readQueueValuesBoolean(int subentry);
+
+  public static native boolean setBoolean(int entry, long time, boolean value);
+
+  public static native boolean getBoolean(int entry, boolean defaultValue);
+
+  public static native boolean setDefaultBoolean(int entry, long time, boolean defaultValue);
+
+
+  public static native TimestampedInteger getAtomicInteger(
+      int subentry, long defaultValue);
+
+  public static native TimestampedInteger[] readQueueInteger(int subentry);
+
+  public static native long[] readQueueValuesInteger(int subentry);
+
+  public static native boolean setInteger(int entry, long time, long value);
+
+  public static native long getInteger(int entry, long defaultValue);
+
+  public static native boolean setDefaultInteger(int entry, long time, long defaultValue);
+
+
+  public static native TimestampedFloat getAtomicFloat(
+      int subentry, float defaultValue);
+
+  public static native TimestampedFloat[] readQueueFloat(int subentry);
+
+  public static native float[] readQueueValuesFloat(int subentry);
+
+  public static native boolean setFloat(int entry, long time, float value);
+
+  public static native float getFloat(int entry, float defaultValue);
+
+  public static native boolean setDefaultFloat(int entry, long time, float defaultValue);
+
+
+  public static native TimestampedDouble getAtomicDouble(
+      int subentry, double defaultValue);
+
+  public static native TimestampedDouble[] readQueueDouble(int subentry);
+
+  public static native double[] readQueueValuesDouble(int subentry);
+
+  public static native boolean setDouble(int entry, long time, double value);
+
+  public static native double getDouble(int entry, double defaultValue);
+
+  public static native boolean setDefaultDouble(int entry, long time, double defaultValue);
+
+
+  public static native TimestampedString getAtomicString(
+      int subentry, String defaultValue);
+
+  public static native TimestampedString[] readQueueString(int subentry);
+
+  public static native String[] readQueueValuesString(int subentry);
+
+  public static native boolean setString(int entry, long time, String value);
+
+  public static native String getString(int entry, String defaultValue);
+
+  public static native boolean setDefaultString(int entry, long time, String defaultValue);
+
+
+  public static native TimestampedRaw getAtomicRaw(
+      int subentry, byte[] defaultValue);
+
+  public static native TimestampedRaw[] readQueueRaw(int subentry);
+
+  public static native byte[][] readQueueValuesRaw(int subentry);
+
   public static boolean setRaw(int entry, long time, byte[] value) {
     return setRaw(entry, time, value, 0, value.length);
   }
@@ -215,11 +305,9 @@
   }
 
   private static native boolean setRawBuffer(int entry, long time, ByteBuffer value, int start, int len);
-{% else %}
-  public static native boolean set{{ t.TypeName }}(int entry, long time, {{ t.java.ValueType }} value);
-{% endif %}
-  public static native {{ t.java.ValueType }} get{{ t.TypeName }}(int entry, {{ t.java.ValueType }} defaultValue);
-{% if t.TypeName == "Raw" %}
+
+  public static native byte[] getRaw(int entry, byte[] defaultValue);
+
   public static boolean setDefaultRaw(int entry, long time, byte[] defaultValue) {
     return setDefaultRaw(entry, time, defaultValue, 0, defaultValue.length);
   }
@@ -251,10 +339,78 @@
   }
 
   private static native boolean setDefaultRawBuffer(int entry, long time, ByteBuffer defaultValue, int start, int len);
-{% else %}
-  public static native boolean setDefault{{ t.TypeName }}(int entry, long time, {{ t.java.ValueType }} defaultValue);
-{% endif %}
-{% endfor %}
+
+
+  public static native TimestampedBooleanArray getAtomicBooleanArray(
+      int subentry, boolean[] defaultValue);
+
+  public static native TimestampedBooleanArray[] readQueueBooleanArray(int subentry);
+
+  public static native boolean[][] readQueueValuesBooleanArray(int subentry);
+
+  public static native boolean setBooleanArray(int entry, long time, boolean[] value);
+
+  public static native boolean[] getBooleanArray(int entry, boolean[] defaultValue);
+
+  public static native boolean setDefaultBooleanArray(int entry, long time, boolean[] defaultValue);
+
+
+  public static native TimestampedIntegerArray getAtomicIntegerArray(
+      int subentry, long[] defaultValue);
+
+  public static native TimestampedIntegerArray[] readQueueIntegerArray(int subentry);
+
+  public static native long[][] readQueueValuesIntegerArray(int subentry);
+
+  public static native boolean setIntegerArray(int entry, long time, long[] value);
+
+  public static native long[] getIntegerArray(int entry, long[] defaultValue);
+
+  public static native boolean setDefaultIntegerArray(int entry, long time, long[] defaultValue);
+
+
+  public static native TimestampedFloatArray getAtomicFloatArray(
+      int subentry, float[] defaultValue);
+
+  public static native TimestampedFloatArray[] readQueueFloatArray(int subentry);
+
+  public static native float[][] readQueueValuesFloatArray(int subentry);
+
+  public static native boolean setFloatArray(int entry, long time, float[] value);
+
+  public static native float[] getFloatArray(int entry, float[] defaultValue);
+
+  public static native boolean setDefaultFloatArray(int entry, long time, float[] defaultValue);
+
+
+  public static native TimestampedDoubleArray getAtomicDoubleArray(
+      int subentry, double[] defaultValue);
+
+  public static native TimestampedDoubleArray[] readQueueDoubleArray(int subentry);
+
+  public static native double[][] readQueueValuesDoubleArray(int subentry);
+
+  public static native boolean setDoubleArray(int entry, long time, double[] value);
+
+  public static native double[] getDoubleArray(int entry, double[] defaultValue);
+
+  public static native boolean setDefaultDoubleArray(int entry, long time, double[] defaultValue);
+
+
+  public static native TimestampedStringArray getAtomicStringArray(
+      int subentry, String[] defaultValue);
+
+  public static native TimestampedStringArray[] readQueueStringArray(int subentry);
+
+  public static native String[][] readQueueValuesStringArray(int subentry);
+
+  public static native boolean setStringArray(int entry, long time, String[] value);
+
+  public static native String[] getStringArray(int entry, String[] defaultValue);
+
+  public static native boolean setDefaultStringArray(int entry, long time, String[] defaultValue);
+
+
   public static native NetworkTableValue[] readQueueValue(int subentry);
 
   public static native NetworkTableValue getValue(int entry);
@@ -352,4 +508,7 @@
   public static native void stopConnectionDataLog(int logger);
 
   public static native int addLogger(int poller, int minLevel, int maxLevel);
+
+  /** Utility class. */
+  private NetworkTablesJNI() {}
 }
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawEntry.java
similarity index 69%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/RawEntry.java
index cbaa782..407cdd5 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Raw entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface RawEntry extends RawSubscriber, RawPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawEntryImpl.java
new file mode 100644
index 0000000..55767f7
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawEntryImpl.java
@@ -0,0 +1,89 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.nio.ByteBuffer;
+
+/** NetworkTables Raw implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class RawEntryImpl extends EntryBase implements RawEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  RawEntryImpl(RawTopic topic, int handle, byte[] defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public RawTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public byte[] get() {
+    return NetworkTablesJNI.getRaw(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public byte[] get(byte[] defaultValue) {
+    return NetworkTablesJNI.getRaw(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedRaw getAtomic() {
+    return NetworkTablesJNI.getAtomicRaw(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedRaw getAtomic(byte[] defaultValue) {
+    return NetworkTablesJNI.getAtomicRaw(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedRaw[] readQueue() {
+    return NetworkTablesJNI.readQueueRaw(m_handle);
+  }
+
+  @Override
+  public byte[][] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesRaw(m_handle);
+  }
+
+  @Override
+  public void set(byte[] value, int start, int len, long time) {
+    NetworkTablesJNI.setRaw(m_handle, time, value, start, len);
+  }
+
+  @Override
+  public void set(ByteBuffer value, int start, int len, long time) {
+    NetworkTablesJNI.setRaw(m_handle, time, value, start, len);
+  }
+
+  @Override
+  public void setDefault(byte[] value, int start, int len) {
+    NetworkTablesJNI.setDefaultRaw(m_handle, 0, value, start, len);
+  }
+
+  @Override
+  public void setDefault(ByteBuffer value, int start, int len) {
+    NetworkTablesJNI.setDefaultRaw(m_handle, 0, value, start, len);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final RawTopic m_topic;
+  private final byte[] m_defaultValue;
+}
diff --git a/ntcore/src/generate/java/Publisher.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawPublisher.java
similarity index 80%
copy from ntcore/src/generate/java/Publisher.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/RawPublisher.java
index a403d91..0cd578e 100644
--- a/ntcore/src/generate/java/Publisher.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawPublisher.java
@@ -2,33 +2,34 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-{% if TypeName == "Raw" %}
-import java.nio.ByteBuffer;
-{% endif -%}
-import {{ java.ConsumerFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Consumer;
 
-/** NetworkTables {{ TypeName }} publisher. */
-public interface {{ TypeName }}Publisher extends Publisher, {{ java.FunctionTypePrefix }}Consumer{{ java.FunctionTypeSuffix }} {
+import java.nio.ByteBuffer;
+import java.util.function.Consumer;
+
+/** NetworkTables Raw publisher. */
+public interface RawPublisher extends Publisher, Consumer<byte[]> {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  RawTopic getTopic();
 
   /**
    * Publish a new value using current NT time.
    *
    * @param value value to publish
    */
-  default void set({{ java.ValueType }} value) {
+  default void set(byte[] value) {
     set(value, 0);
   }
 
-{% if TypeName == "Raw" %}
+
   /**
    * Publish a new value.
    *
@@ -145,26 +146,9 @@
    * @param len Length of data (must be less than or equal to value.capacity() - start)
    */
   void setDefault(ByteBuffer value, int start, int len);
-{% else %}
-  /**
-   * Publish a new value.
-   *
-   * @param value value to publish
-   * @param time timestamp; 0 indicates current NT time should be used
-   */
-  void set({{ java.ValueType }} value, long time);
 
-  /**
-   * Publish a default value.
-   * On reconnect, a default value will never be used in preference to a
-   * published value.
-   *
-   * @param value value
-   */
-  void setDefault({{ java.ValueType }} value);
-{% endif %}
   @Override
-  default void accept({{ java.ValueType }} value) {
+  default void accept(byte[] value) {
     set(value);
   }
 }
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawSubscriber.java
similarity index 70%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/RawSubscriber.java
index 0ea09a3..089d999 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawSubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.Supplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables Raw subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface RawSubscriber extends Subscriber, Supplier<byte[]> {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  RawTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  byte[] get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,8 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
-  @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
-    return get();
-  }
-{% endif %}
+  byte[] get(byte[] defaultValue);
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +43,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedRaw getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +53,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedRaw getAtomic(byte[] defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +65,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedRaw[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +76,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  byte[][] readQueueValues();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawTopic.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawTopic.java
new file mode 100644
index 0000000..173a88b
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/RawTopic.java
@@ -0,0 +1,154 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables Raw topic. */
+public final class RawTopic extends Topic {
+  /**
+   * Construct from a generic topic.
+   *
+   * @param topic Topic
+   */
+  public RawTopic(Topic topic) {
+    super(topic.m_inst, topic.m_handle);
+  }
+
+  /**
+   * Constructor; use NetworkTableInstance.getRawTopic() instead.
+   *
+   * @param inst Instance
+   * @param handle Native handle
+   */
+  public RawTopic(NetworkTableInstance inst, int handle) {
+    super(inst, handle);
+  }
+
+  /**
+   * Create a new subscriber to the topic.
+   *
+   * <p>The subscriber is only active as long as the returned object
+   * is not closed.
+   *
+   * <p>Subscribers that do not match the published data type do not return
+   * any values. To determine if the data type matches, use the appropriate
+   * Topic functions.
+   *
+   * @param typeString type string
+
+   * @param defaultValue default value used when a default is not provided to a
+   *        getter function
+   * @param options subscribe options
+   * @return subscriber
+   */
+  public RawSubscriber subscribe(
+      String typeString,
+
+      byte[] defaultValue,
+      PubSubOption... options) {
+    return new RawEntryImpl(
+        this,
+        NetworkTablesJNI.subscribe(
+            m_handle, NetworkTableType.kRaw.getValue(),
+            typeString, options),
+        defaultValue);
+  }
+
+  /**
+   * Create a new publisher to the topic.
+   *
+   * <p>The publisher is only active as long as the returned object
+   * is not closed.
+   *
+   * <p>It is not possible to publish two different data types to the same
+   * topic. Conflicts between publishers are typically resolved by the server on
+   * a first-come, first-served basis. Any published values that do not match
+   * the topic's data type are dropped (ignored). To determine if the data type
+   * matches, use the appropriate Topic functions.
+   *
+   * @param typeString type string
+
+   * @param options publish options
+   * @return publisher
+   */
+  public RawPublisher publish(
+      String typeString,
+
+      PubSubOption... options) {
+    return new RawEntryImpl(
+        this,
+        NetworkTablesJNI.publish(
+            m_handle, NetworkTableType.kRaw.getValue(),
+            typeString, options),
+        new byte[] {});
+  }
+
+  /**
+   * Create a new publisher to the topic, with type string and initial properties.
+   *
+   * <p>The publisher is only active as long as the returned object
+   * is not closed.
+   *
+   * <p>It is not possible to publish two different data types to the same
+   * topic. Conflicts between publishers are typically resolved by the server on
+   * a first-come, first-served basis. Any published values that do not match
+   * the topic's data type are dropped (ignored). To determine if the data type
+   * matches, use the appropriate Topic functions.
+   *
+   * @param typeString type string
+   * @param properties JSON properties
+   * @param options publish options
+   * @return publisher
+   * @throws IllegalArgumentException if properties is not a JSON object
+   */
+  public RawPublisher publishEx(
+      String typeString,
+      String properties,
+      PubSubOption... options) {
+    return new RawEntryImpl(
+        this,
+        NetworkTablesJNI.publishEx(
+            m_handle, NetworkTableType.kRaw.getValue(),
+            typeString, properties, options),
+        new byte[] {});
+  }
+
+  /**
+   * Create a new entry for the topic.
+   *
+   * <p>Entries act as a combination of a subscriber and a weak publisher. The
+   * subscriber is active as long as the entry is not closed. The publisher is
+   * created when the entry is first written to, and remains active until either
+   * unpublish() is called or the entry is closed.
+   *
+   * <p>It is not possible to use two different data types with the same
+   * topic. Conflicts between publishers are typically resolved by the server on
+   * a first-come, first-served basis. Any published values that do not match
+   * the topic's data type are dropped (ignored), and the entry will show no new
+   * values if the data type does not match. To determine if the data type
+   * matches, use the appropriate Topic functions.
+   *
+   * @param typeString type string
+
+   * @param defaultValue default value used when a default is not provided to a
+   *        getter function
+   * @param options publish and/or subscribe options
+   * @return entry
+   */
+  public RawEntry getEntry(
+      String typeString,
+
+      byte[] defaultValue,
+      PubSubOption... options) {
+    return new RawEntryImpl(
+        this,
+        NetworkTablesJNI.getEntry(
+            m_handle, NetworkTableType.kRaw.getValue(),
+            typeString, options),
+        defaultValue);
+  }
+
+}
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayEntry.java
similarity index 65%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayEntry.java
index cbaa782..9169e90 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables StringArray entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface StringArrayEntry extends StringArraySubscriber, StringArrayPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayEntryImpl.java
new file mode 100644
index 0000000..536db7b
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables StringArray implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class StringArrayEntryImpl extends EntryBase implements StringArrayEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  StringArrayEntryImpl(StringArrayTopic topic, int handle, String[] defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public StringArrayTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public String[] get() {
+    return NetworkTablesJNI.getStringArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public String[] get(String[] defaultValue) {
+    return NetworkTablesJNI.getStringArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedStringArray getAtomic() {
+    return NetworkTablesJNI.getAtomicStringArray(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedStringArray getAtomic(String[] defaultValue) {
+    return NetworkTablesJNI.getAtomicStringArray(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedStringArray[] readQueue() {
+    return NetworkTablesJNI.readQueueStringArray(m_handle);
+  }
+
+  @Override
+  public String[][] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesStringArray(m_handle);
+  }
+
+  @Override
+  public void set(String[] value, long time) {
+    NetworkTablesJNI.setStringArray(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(String[] value) {
+    NetworkTablesJNI.setDefaultStringArray(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final StringArrayTopic m_topic;
+  private final String[] m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayPublisher.java
new file mode 100644
index 0000000..d32350c
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.Consumer;
+
+/** NetworkTables StringArray publisher. */
+public interface StringArrayPublisher extends Publisher, Consumer<String[]> {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  StringArrayTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(String[] value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(String[] value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(String[] value);
+
+  @Override
+  default void accept(String[] value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArraySubscriber.java
similarity index 70%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArraySubscriber.java
index 0ea09a3..62a0575 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArraySubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.Supplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables StringArray subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface StringArraySubscriber extends Subscriber, Supplier<String[]> {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  StringArrayTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  String[] get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,8 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
-  @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
-    return get();
-  }
-{% endif %}
+  String[] get(String[] defaultValue);
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +43,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedStringArray getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +53,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedStringArray getAtomic(String[] defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +65,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedStringArray[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +76,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  String[][] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayTopic.java
index e22fa3b..bde93c0 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringArrayTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables StringArray topic. */
+public final class StringArrayTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "string[]";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public StringArrayTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getStringArrayTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public StringArrayTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public StringArraySubscriber subscribe(
+      String[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kStringArray.getValue(),
+            "string[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public StringArraySubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      String[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringArrayEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kStringArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public StringArrayPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringArrayEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kStringArray.getValue(),
+            "string[]", options),
+        new String[] {});
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public StringArrayPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringArrayEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kStringArray.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        new String[] {});
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public StringArrayEntry getEntry(
+      String[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kStringArray.getValue(),
+            "string[]", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public StringArrayEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      String[] defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringArrayEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kStringArray.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringEntry.java
similarity index 68%
copy from ntcore/src/generate/java/Entry.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/StringEntry.java
index cbaa782..6914179 100644
--- a/ntcore/src/generate/java/Entry.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringEntry.java
@@ -2,14 +2,16 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables String entry.
  *
  * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
  */
-public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+public interface StringEntry extends StringSubscriber, StringPublisher {
   /** Stops publishing the entry if it's published. */
   void unpublish();
 }
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringEntryImpl.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringEntryImpl.java
new file mode 100644
index 0000000..76c9b98
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringEntryImpl.java
@@ -0,0 +1,77 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables String implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class StringEntryImpl extends EntryBase implements StringEntry {
+  /**
+   * Constructor.
+   *
+   * @param topic Topic
+   * @param handle Native handle
+   * @param defaultValue Default value for get()
+   */
+  StringEntryImpl(StringTopic topic, int handle, String defaultValue) {
+    super(handle);
+    m_topic = topic;
+    m_defaultValue = defaultValue;
+  }
+
+  @Override
+  public StringTopic getTopic() {
+    return m_topic;
+  }
+
+  @Override
+  public String get() {
+    return NetworkTablesJNI.getString(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public String get(String defaultValue) {
+    return NetworkTablesJNI.getString(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedString getAtomic() {
+    return NetworkTablesJNI.getAtomicString(m_handle, m_defaultValue);
+  }
+
+  @Override
+  public TimestampedString getAtomic(String defaultValue) {
+    return NetworkTablesJNI.getAtomicString(m_handle, defaultValue);
+  }
+
+  @Override
+  public TimestampedString[] readQueue() {
+    return NetworkTablesJNI.readQueueString(m_handle);
+  }
+
+  @Override
+  public String[] readQueueValues() {
+    return NetworkTablesJNI.readQueueValuesString(m_handle);
+  }
+
+  @Override
+  public void set(String value, long time) {
+    NetworkTablesJNI.setString(m_handle, time, value);
+  }
+
+  @Override
+  public void setDefault(String value) {
+    NetworkTablesJNI.setDefaultString(m_handle, 0, value);
+  }
+
+  @Override
+  public void unpublish() {
+    NetworkTablesJNI.unpublish(m_handle);
+  }
+
+  private final StringTopic m_topic;
+  private final String m_defaultValue;
+}
diff --git a/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringPublisher.java b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringPublisher.java
new file mode 100644
index 0000000..c11f11c
--- /dev/null
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringPublisher.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+package edu.wpi.first.networktables;
+
+import java.util.function.Consumer;
+
+/** NetworkTables String publisher. */
+public interface StringPublisher extends Publisher, Consumer<String> {
+  /**
+   * Get the corresponding topic.
+   *
+   * @return Topic
+   */
+  @Override
+  StringTopic getTopic();
+
+  /**
+   * Publish a new value using current NT time.
+   *
+   * @param value value to publish
+   */
+  default void set(String value) {
+    set(value, 0);
+  }
+
+
+  /**
+   * Publish a new value.
+   *
+   * @param value value to publish
+   * @param time timestamp; 0 indicates current NT time should be used
+   */
+  void set(String value, long time);
+
+  /**
+   * Publish a default value.
+   * On reconnect, a default value will never be used in preference to a
+   * published value.
+   *
+   * @param value value
+   */
+  void setDefault(String value);
+
+  @Override
+  default void accept(String value) {
+    set(value);
+  }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringSubscriber.java
similarity index 70%
copy from ntcore/src/generate/java/Subscriber.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/StringSubscriber.java
index 0ea09a3..8c453b0 100644
--- a/ntcore/src/generate/java/Subscriber.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringSubscriber.java
@@ -2,20 +2,22 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+import java.util.function.Supplier;
 
-/** NetworkTables {{ TypeName }} subscriber. */
+/** NetworkTables String subscriber. */
 @SuppressWarnings("PMD.MissingOverride")
-public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+public interface StringSubscriber extends Subscriber, Supplier<String> {
   /**
    * Get the corresponding topic.
    *
    * @return Topic
    */
   @Override
-  {{ TypeName }}Topic getTopic();
+  StringTopic getTopic();
 
   /**
    * Get the last published value.
@@ -23,7 +25,7 @@
    *
    * @return value
    */
-  {{ java.ValueType }} get();
+  String get();
 
   /**
    * Get the last published value.
@@ -32,13 +34,8 @@
    * @param defaultValue default value to return if no value has been published
    * @return value
    */
-  {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
-{% if java.FunctionTypePrefix %}
-  @Override
-  default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
-    return get();
-  }
-{% endif %}
+  String get(String defaultValue);
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -46,7 +43,7 @@
    *
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic();
+  TimestampedString getAtomic();
 
   /**
    * Get the last published value along with its timestamp
@@ -56,7 +53,7 @@
    * @param defaultValue default value to return if no value has been published
    * @return timestamped value
    */
-  Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+  TimestampedString getAtomic(String defaultValue);
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -68,7 +65,7 @@
    * @return Array of timestamped values; empty array if no new changes have
    *     been published since the previous call.
    */
-  Timestamped{{ TypeName }}[] readQueue();
+  TimestampedString[] readQueue();
 
   /**
    * Get an array of all value changes since the last call to readQueue.
@@ -79,5 +76,5 @@
    * @return Array of values; empty array if no new changes have been
    *     published since the previous call.
    */
-  {{ java.ValueType }}[] readQueueValues();
+  String[] readQueueValues();
 }
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringTopic.java
similarity index 72%
copy from ntcore/src/generate/java/Topic.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/StringTopic.java
index e22fa3b..a634180 100644
--- a/ntcore/src/generate/java/Topic.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/StringTopic.java
@@ -2,30 +2,31 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables {{ TypeName }} topic. */
-public final class {{ TypeName }}Topic extends Topic {
-{%- if TypeString %}
+/** NetworkTables String topic. */
+public final class StringTopic extends Topic {
   /** The default type string for this topic type. */
-  public static final String kTypeString = {{ TypeString }};
-{% endif %}
+  public static final String kTypeString = "string";
+
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  public {{ TypeName }}Topic(Topic topic) {
+  public StringTopic(Topic topic) {
     super(topic.m_inst, topic.m_handle);
   }
 
   /**
-   * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+   * Constructor; use NetworkTableInstance.getStringTopic() instead.
    *
    * @param inst Instance
    * @param handle Native handle
    */
-  public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+  public StringTopic(NetworkTableInstance inst, int handle) {
     super(inst, handle);
   }
 
@@ -39,28 +40,22 @@
    * any values. To determine if the data type matches, use the appropriate
    * Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribe(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public StringSubscriber subscribe(
+      String defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kString.getValue(),
+            "string", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new subscriber to the topic, with specified type string.
    *
@@ -77,18 +72,18 @@
    * @param options subscribe options
    * @return subscriber
    */
-  public {{ TypeName }}Subscriber subscribeEx(
+  public StringSubscriber subscribeEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      String defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringEntryImpl(
         this,
         NetworkTablesJNI.subscribe(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kString.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -101,23 +96,17 @@
    * the topic's data type are dropped (ignored). To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
-  public {{ TypeName }}Publisher publish(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
+  public StringPublisher publish(
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringEntryImpl(
         this,
         NetworkTablesJNI.publish(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
-        {{ java.EmptyValue }});
+            m_handle, NetworkTableType.kString.getValue(),
+            "string", options),
+        "");
   }
 
   /**
@@ -138,16 +127,16 @@
    * @return publisher
    * @throws IllegalArgumentException if properties is not a JSON object
    */
-  public {{ TypeName }}Publisher publishEx(
+  public StringPublisher publishEx(
       String typeString,
       String properties,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringEntryImpl(
         this,
         NetworkTablesJNI.publishEx(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kString.getValue(),
             typeString, properties, options),
-        {{ java.EmptyValue }});
+        "");
   }
 
   /**
@@ -165,28 +154,22 @@
    * values if the data type does not match. To determine if the data type
    * matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntry(
-{%- if not TypeString %}
-      String typeString,
-{% endif %}
-      {{ java.ValueType }} defaultValue,
+  public StringEntry getEntry(
+      String defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
-            {{ TypeString|default('typeString') }}, options),
+            m_handle, NetworkTableType.kString.getValue(),
+            "string", options),
         defaultValue);
   }
-{% if TypeString %}
+
   /**
    * Create a new entry for the topic, with specified type string.
    *
@@ -208,16 +191,16 @@
    * @param options publish and/or subscribe options
    * @return entry
    */
-  public {{ TypeName }}Entry getEntryEx(
+  public StringEntry getEntryEx(
       String typeString,
-      {{ java.ValueType }} defaultValue,
+      String defaultValue,
       PubSubOption... options) {
-    return new {{ TypeName }}EntryImpl(
+    return new StringEntryImpl(
         this,
         NetworkTablesJNI.getEntry(
-            m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+            m_handle, NetworkTableType.kString.getValue(),
             typeString, options),
         defaultValue);
   }
-{% endif %}
+
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedBoolean.java
similarity index 76%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedBoolean.java
index 288af81..05497fc 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedBoolean.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped Boolean. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedBoolean {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedBoolean(long timestamp, long serverTime, boolean value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final boolean value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedBooleanArray.java
similarity index 74%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedBooleanArray.java
index 288af81..51e2591 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedBooleanArray.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped BooleanArray. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedBooleanArray {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedBooleanArray(long timestamp, long serverTime, boolean[] value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final boolean[] value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedDouble.java
similarity index 76%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedDouble.java
index 288af81..586c9fc 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedDouble.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped Double. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedDouble {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedDouble(long timestamp, long serverTime, double value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final double value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedDoubleArray.java
similarity index 75%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedDoubleArray.java
index 288af81..aaeafd7 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedDoubleArray.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped DoubleArray. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedDoubleArray {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedDoubleArray(long timestamp, long serverTime, double[] value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final double[] value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedFloat.java
similarity index 76%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedFloat.java
index 288af81..4d8aa09 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedFloat.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped Float. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedFloat {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedFloat(long timestamp, long serverTime, float value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final float value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedFloatArray.java
similarity index 75%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedFloatArray.java
index 288af81..85fe7bc 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedFloatArray.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped FloatArray. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedFloatArray {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedFloatArray(long timestamp, long serverTime, float[] value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final float[] value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedInteger.java
similarity index 76%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedInteger.java
index 288af81..0ee7855 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedInteger.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped Integer. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedInteger {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedInteger(long timestamp, long serverTime, long value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final long value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedIntegerArray.java
similarity index 75%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedIntegerArray.java
index 288af81..8686326 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedIntegerArray.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped IntegerArray. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedIntegerArray {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedIntegerArray(long timestamp, long serverTime, long[] value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final long[] value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedRaw.java
similarity index 76%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedRaw.java
index 288af81..12ec095 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedRaw.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped Raw. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedRaw {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedRaw(long timestamp, long serverTime, byte[] value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final byte[] value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedString.java
similarity index 76%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedString.java
index 288af81..a51432e 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedString.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped String. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedString {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedString(long timestamp, long serverTime, String value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final String value;
 }
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedStringArray.java
similarity index 75%
copy from ntcore/src/generate/java/Timestamped.java.jinja
copy to ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedStringArray.java
index 288af81..420a468 100644
--- a/ntcore/src/generate/java/Timestamped.java.jinja
+++ b/ntcore/src/generated/main/java/edu/wpi/first/networktables/TimestampedStringArray.java
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped {{ TypeName }}. */
+/** NetworkTables timestamped StringArray. */
 @SuppressWarnings("PMD.ArrayIsStoredDirectly")
-public final class Timestamped{{ TypeName }} {
+public final class TimestampedStringArray {
   /**
    * Create a timestamped value.
    *
@@ -14,7 +16,7 @@
    * @param serverTime timestamp in server time base
    * @param value value
    */
-  public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} value) {
+  public TimestampedStringArray(long timestamp, long serverTime, String[] value) {
     this.timestamp = timestamp;
     this.serverTime = serverTime;
     this.value = value;
@@ -36,5 +38,5 @@
    * Value.
    */
   @SuppressWarnings("MemberName")
-  public final {{ java.ValueType }} value;
+  public final String[] value;
 }
diff --git a/ntcore/src/generated/main/native/cpp/jni/types_jni.cpp b/ntcore/src/generated/main/native/cpp/jni/types_jni.cpp
new file mode 100644
index 0000000..de14d53
--- /dev/null
+++ b/ntcore/src/generated/main/native/cpp/jni/types_jni.cpp
@@ -0,0 +1,1436 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#include <jni.h>
+
+#include <wpi/jni_util.h>
+
+#include "edu_wpi_first_networktables_NetworkTablesJNI.h"
+#include "ntcore.h"
+
+using namespace wpi::java;
+
+//
+// Globals and load/unload
+//
+
+static JClass timestampedBooleanCls;
+static JClass timestampedIntegerCls;
+static JClass timestampedFloatCls;
+static JClass timestampedDoubleCls;
+static JClass timestampedStringCls;
+static JClass timestampedRawCls;
+static JClass timestampedBooleanArrayCls;
+static JClass timestampedIntegerArrayCls;
+static JClass timestampedFloatArrayCls;
+static JClass timestampedDoubleArrayCls;
+static JClass timestampedStringArrayCls;
+static JClass jbyteArrayCls;
+static JClass jbooleanArrayCls;
+static JClass jlongArrayCls;
+static JClass jfloatArrayCls;
+static JClass jdoubleArrayCls;
+static JClass jobjectArrayCls;
+static JException illegalArgEx;
+static JException indexOobEx;
+static JException nullPointerEx;
+
+static const JClassInit classes[] = {
+    {"edu/wpi/first/networktables/TimestampedBoolean", &timestampedBooleanCls},
+    {"edu/wpi/first/networktables/TimestampedInteger", &timestampedIntegerCls},
+    {"edu/wpi/first/networktables/TimestampedFloat", &timestampedFloatCls},
+    {"edu/wpi/first/networktables/TimestampedDouble", &timestampedDoubleCls},
+    {"edu/wpi/first/networktables/TimestampedString", &timestampedStringCls},
+    {"edu/wpi/first/networktables/TimestampedRaw", &timestampedRawCls},
+    {"edu/wpi/first/networktables/TimestampedBooleanArray", &timestampedBooleanArrayCls},
+    {"edu/wpi/first/networktables/TimestampedIntegerArray", &timestampedIntegerArrayCls},
+    {"edu/wpi/first/networktables/TimestampedFloatArray", &timestampedFloatArrayCls},
+    {"edu/wpi/first/networktables/TimestampedDoubleArray", &timestampedDoubleArrayCls},
+    {"edu/wpi/first/networktables/TimestampedStringArray", &timestampedStringArrayCls},
+    {"[B", &jbyteArrayCls},
+    {"[Z", &jbooleanArrayCls},
+    {"[J", &jlongArrayCls},
+    {"[F", &jfloatArrayCls},
+    {"[D", &jdoubleArrayCls},
+    {"[Ljava/lang/Object;", &jobjectArrayCls},
+};
+
+static const JExceptionInit exceptions[] = {
+    {"java/lang/IllegalArgumentException", &illegalArgEx},
+    {"java/lang/IndexOutOfBoundsException", &indexOobEx},
+    {"java/lang/NullPointerException", &nullPointerEx},
+};
+
+namespace nt {
+
+bool JNI_LoadTypes(JNIEnv* env) {
+  // Cache references to classes
+  for (auto& c : classes) {
+    *c.cls = JClass(env, c.name);
+    if (!*c.cls) {
+      return false;
+    }
+  }
+
+  for (auto& c : exceptions) {
+    *c.cls = JException(env, c.name);
+    if (!*c.cls) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+void JNI_UnloadTypes(JNIEnv* env) {
+  // Delete global references
+  for (auto& c : classes) {
+    c.cls->free(env);
+  }
+  for (auto& c : exceptions) {
+    c.cls->free(env);
+  }
+}
+
+}  // namespace nt
+
+static std::vector<int> FromJavaBooleanArray(JNIEnv* env, jbooleanArray jarr) {
+  CriticalJSpan<const jboolean> ref{env, jarr};
+  if (!ref) {
+    return {};
+  }
+  std::span<const jboolean> elements{ref};
+  size_t len = elements.size();
+  std::vector<int> arr;
+  arr.reserve(len);
+  for (size_t i = 0; i < len; ++i) {
+    arr.push_back(elements[i]);
+  }
+  return arr;
+}
+
+static std::vector<std::string> FromJavaStringArray(JNIEnv* env, jobjectArray jarr) {
+  size_t len = env->GetArrayLength(jarr);
+  std::vector<std::string> arr;
+  arr.reserve(len);
+  for (size_t i = 0; i < len; ++i) {
+    JLocal<jstring> elem{
+        env, static_cast<jstring>(env->GetObjectArrayElement(jarr, i))};
+    if (!elem) {
+      return {};
+    }
+    arr.emplace_back(JStringRef{env, elem}.str());
+  }
+  return arr;
+}
+
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedBoolean value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedBooleanCls, "<init>", "(JJZ)V");
+  return env->NewObject(timestampedBooleanCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime),
+                        static_cast<jboolean>(value.value));
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedInteger value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedIntegerCls, "<init>", "(JJJ)V");
+  return env->NewObject(timestampedIntegerCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime),
+                        static_cast<jlong>(value.value));
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedFloat value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedFloatCls, "<init>", "(JJF)V");
+  return env->NewObject(timestampedFloatCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime),
+                        static_cast<jfloat>(value.value));
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedDouble value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedDoubleCls, "<init>", "(JJD)V");
+  return env->NewObject(timestampedDoubleCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime),
+                        static_cast<jdouble>(value.value));
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedString value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedStringCls, "<init>", "(JJLjava/lang/String;)V");
+  JLocal<jstring> val{env, MakeJString(env, value.value)};
+  return env->NewObject(timestampedStringCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime), val.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedRaw value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedRawCls, "<init>", "(JJ[B)V");
+  JLocal<jbyteArray> val{env, MakeJByteArray(env, value.value)};
+  return env->NewObject(timestampedRawCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime), val.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedBooleanArray value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedBooleanArrayCls, "<init>", "(JJ[Z)V");
+  JLocal<jbooleanArray> val{env, MakeJBooleanArray(env, value.value)};
+  return env->NewObject(timestampedBooleanArrayCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime), val.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedIntegerArray value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedIntegerArrayCls, "<init>", "(JJ[J)V");
+  JLocal<jlongArray> val{env, MakeJLongArray(env, value.value)};
+  return env->NewObject(timestampedIntegerArrayCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime), val.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedFloatArray value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedFloatArrayCls, "<init>", "(JJ[F)V");
+  JLocal<jfloatArray> val{env, MakeJFloatArray(env, value.value)};
+  return env->NewObject(timestampedFloatArrayCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime), val.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedDoubleArray value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedDoubleArrayCls, "<init>", "(JJ[D)V");
+  JLocal<jdoubleArray> val{env, MakeJDoubleArray(env, value.value)};
+  return env->NewObject(timestampedDoubleArrayCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime), val.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, nt::TimestampedStringArray value) {
+  static jmethodID constructor = env->GetMethodID(
+      timestampedStringArrayCls, "<init>", "(JJ[Ljava/lang/Object;)V");
+  JLocal<jobjectArray> val{env, MakeJStringArray(env, value.value)};
+  return env->NewObject(timestampedStringArrayCls, constructor,
+                        static_cast<jlong>(value.time),
+                        static_cast<jlong>(value.serverTime), val.obj());
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedBoolean> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedBooleanCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedInteger> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedIntegerCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedFloat> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedFloatCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedDouble> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedDoubleCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedString> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedStringCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedRaw> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedRawCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedBooleanArray> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedBooleanArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedIntegerArray> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedIntegerArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedFloatArray> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedFloatArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedDoubleArray> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedDoubleArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env,
+                                std::span<const nt::TimestampedStringArray> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), timestampedStringArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObjectArray(JNIEnv* env, std::span<const std::vector<uint8_t>> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), jbyteArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJByteArray(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObjectArray(JNIEnv* env, std::span<const std::vector<int>> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), jbooleanArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJBooleanArray(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObjectArray(JNIEnv* env, std::span<const std::vector<int64_t>> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), jlongArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJLongArray(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObjectArray(JNIEnv* env, std::span<const std::vector<float>> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), jfloatArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJFloatArray(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObjectArray(JNIEnv* env, std::span<const std::vector<double>> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), jdoubleArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJDoubleArray(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObjectArray(JNIEnv* env, std::span<const std::vector<std::string>> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), jobjectArrayCls, nullptr);
+  if (!jarr) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJStringArray(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+
+extern "C" {
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicBoolean
+ * Signature: (IZ)Ledu/wpi/first/networktables/TimestampedBoolean;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicBoolean
+  (JNIEnv* env, jclass, jint subentry, jboolean defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicBoolean(subentry, defaultValue != JNI_FALSE));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueBoolean
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedBoolean;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueBoolean
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueBoolean(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesBoolean
+ * Signature: (I)[Z
+ */
+JNIEXPORT jbooleanArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesBoolean
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJBooleanArray(env, nt::ReadQueueValuesBoolean(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setBoolean
+ * Signature: (IJZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setBoolean
+  (JNIEnv*, jclass, jint entry, jlong time, jboolean value)
+{
+  return nt::SetBoolean(entry, value != JNI_FALSE, time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getBoolean
+ * Signature: (IZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getBoolean
+  (JNIEnv*, jclass, jint entry, jboolean defaultValue)
+{
+  return nt::GetBoolean(entry, defaultValue);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultBoolean
+ * Signature: (IJZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultBoolean
+  (JNIEnv*, jclass, jint entry, jlong, jboolean defaultValue)
+{
+  return nt::SetDefaultBoolean(entry, defaultValue != JNI_FALSE);
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicInteger
+ * Signature: (IJ)Ledu/wpi/first/networktables/TimestampedInteger;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicInteger
+  (JNIEnv* env, jclass, jint subentry, jlong defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicInteger(subentry, defaultValue));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueInteger
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedInteger;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueInteger
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueInteger(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesInteger
+ * Signature: (I)[J
+ */
+JNIEXPORT jlongArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesInteger
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJLongArray(env, nt::ReadQueueValuesInteger(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setInteger
+ * Signature: (IJJ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setInteger
+  (JNIEnv*, jclass, jint entry, jlong time, jlong value)
+{
+  return nt::SetInteger(entry, value, time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getInteger
+ * Signature: (IJ)J
+ */
+JNIEXPORT jlong JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getInteger
+  (JNIEnv*, jclass, jint entry, jlong defaultValue)
+{
+  return nt::GetInteger(entry, defaultValue);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultInteger
+ * Signature: (IJJ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultInteger
+  (JNIEnv*, jclass, jint entry, jlong, jlong defaultValue)
+{
+  return nt::SetDefaultInteger(entry, defaultValue);
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicFloat
+ * Signature: (IF)Ledu/wpi/first/networktables/TimestampedFloat;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicFloat
+  (JNIEnv* env, jclass, jint subentry, jfloat defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicFloat(subentry, defaultValue));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueFloat
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedFloat;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueFloat
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueFloat(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesFloat
+ * Signature: (I)[F
+ */
+JNIEXPORT jfloatArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesFloat
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJFloatArray(env, nt::ReadQueueValuesFloat(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setFloat
+ * Signature: (IJF)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setFloat
+  (JNIEnv*, jclass, jint entry, jlong time, jfloat value)
+{
+  return nt::SetFloat(entry, value, time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getFloat
+ * Signature: (IF)F
+ */
+JNIEXPORT jfloat JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getFloat
+  (JNIEnv*, jclass, jint entry, jfloat defaultValue)
+{
+  return nt::GetFloat(entry, defaultValue);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultFloat
+ * Signature: (IJF)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultFloat
+  (JNIEnv*, jclass, jint entry, jlong, jfloat defaultValue)
+{
+  return nt::SetDefaultFloat(entry, defaultValue);
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicDouble
+ * Signature: (ID)Ledu/wpi/first/networktables/TimestampedDouble;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicDouble
+  (JNIEnv* env, jclass, jint subentry, jdouble defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicDouble(subentry, defaultValue));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueDouble
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedDouble;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueDouble
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueDouble(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesDouble
+ * Signature: (I)[D
+ */
+JNIEXPORT jdoubleArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesDouble
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJDoubleArray(env, nt::ReadQueueValuesDouble(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDouble
+ * Signature: (IJD)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDouble
+  (JNIEnv*, jclass, jint entry, jlong time, jdouble value)
+{
+  return nt::SetDouble(entry, value, time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getDouble
+ * Signature: (ID)D
+ */
+JNIEXPORT jdouble JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getDouble
+  (JNIEnv*, jclass, jint entry, jdouble defaultValue)
+{
+  return nt::GetDouble(entry, defaultValue);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultDouble
+ * Signature: (IJD)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultDouble
+  (JNIEnv*, jclass, jint entry, jlong, jdouble defaultValue)
+{
+  return nt::SetDefaultDouble(entry, defaultValue);
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicString
+ * Signature: (ILjava/lang/String;)Ledu/wpi/first/networktables/TimestampedString;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicString
+  (JNIEnv* env, jclass, jint subentry, jstring defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicString(subentry, JStringRef{env, defaultValue}));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueString
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedString;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueString
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueString(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesString
+ * Signature: (I)[Ljava/lang/String;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesString
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJStringArray(env, nt::ReadQueueValuesString(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setString
+ * Signature: (IJLjava/lang/String;)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setString
+  (JNIEnv* env, jclass, jint entry, jlong time, jstring value)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  return nt::SetString(entry, JStringRef{env, value}, time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getString
+ * Signature: (ILjava/lang/String;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getString
+  (JNIEnv* env, jclass, jint entry, jstring defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val.IsString()) {
+    return defaultValue;
+  }
+  return MakeJString(env, val.GetString());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultString
+ * Signature: (IJLjava/lang/String;)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultString
+  (JNIEnv* env, jclass, jint entry, jlong, jstring defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  return nt::SetDefaultString(entry, JStringRef{env, defaultValue});
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicRaw
+ * Signature: (I[B)Ledu/wpi/first/networktables/TimestampedRaw;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicRaw
+  (JNIEnv* env, jclass, jint subentry, jbyteArray defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicRaw(subentry, CriticalJSpan<const jbyte>{env, defaultValue}.uarray()));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueRaw
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedRaw;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueRaw
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueRaw(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesRaw
+ * Signature: (I)[[B
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesRaw
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObjectArray(env, nt::ReadQueueValuesRaw(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setRaw
+ * Signature: (IJ[BII)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setRaw
+  (JNIEnv* env, jclass, jint entry, jlong time, jbyteArray value, jint start, jint len)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value is null");
+    return false;
+  }
+  if (start < 0) {
+    indexOobEx.Throw(env, "start must be >= 0");
+    return false;
+  }
+  if (len < 0) {
+    indexOobEx.Throw(env, "len must be >= 0");
+    return false;
+  }
+  CriticalJSpan<const jbyte> cvalue{env, value};
+  if (static_cast<unsigned int>(start + len) > cvalue.size()) {
+    indexOobEx.Throw(env, "start + len must be smaller than array length");
+    return false;
+  }
+  return nt::SetRaw(entry, cvalue.uarray().subspan(start, len), time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setRawBuffer
+ * Signature: (IJLjava/nio/ByteBuffer;II)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setRawBuffer
+  (JNIEnv* env, jclass, jint entry, jlong time, jobject value, jint start, jint len)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value is null");
+    return false;
+  }
+  if (start < 0) {
+    indexOobEx.Throw(env, "start must be >= 0");
+    return false;
+  }
+  if (len < 0) {
+    indexOobEx.Throw(env, "len must be >= 0");
+    return false;
+  }
+  JSpan<const jbyte> cvalue{env, value, static_cast<size_t>(start + len)};
+  if (!cvalue) {
+    illegalArgEx.Throw(env, "value must be a native ByteBuffer");
+    return false;
+  }
+  return nt::SetRaw(entry, cvalue.uarray().subspan(start, len), time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getRaw
+ * Signature: (I[B)[B
+ */
+JNIEXPORT jbyteArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getRaw
+  (JNIEnv* env, jclass, jint entry, jbyteArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val.IsRaw()) {
+    return defaultValue;
+  }
+  return MakeJByteArray(env, val.GetRaw());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultRaw
+ * Signature: (IJ[BII)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultRaw
+  (JNIEnv* env, jclass, jint entry, jlong, jbyteArray defaultValue, jint start, jint len)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "value is null");
+    return false;
+  }
+  if (start < 0) {
+    indexOobEx.Throw(env, "start must be >= 0");
+    return false;
+  }
+  if (len < 0) {
+    indexOobEx.Throw(env, "len must be >= 0");
+    return false;
+  }
+  CriticalJSpan<const jbyte> cvalue{env, defaultValue};
+  if (static_cast<unsigned int>(start + len) > cvalue.size()) {
+    indexOobEx.Throw(env, "start + len must be smaller than array length");
+    return false;
+  }
+  return nt::SetDefaultRaw(entry, cvalue.uarray().subspan(start, len));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultRawBuffer
+ * Signature: (IJLjava/nio/ByteBuffer;II)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultRawBuffer
+  (JNIEnv* env, jclass, jint entry, jlong, jobject defaultValue, jint start, jint len)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "value is null");
+    return false;
+  }
+  if (start < 0) {
+    indexOobEx.Throw(env, "start must be >= 0");
+    return false;
+  }
+  if (len < 0) {
+    indexOobEx.Throw(env, "len must be >= 0");
+    return false;
+  }
+  JSpan<const jbyte> cvalue{env, defaultValue, static_cast<size_t>(start + len)};
+  if (!cvalue) {
+    illegalArgEx.Throw(env, "value must be a native ByteBuffer");
+    return false;
+  }
+  return nt::SetDefaultRaw(entry, cvalue.uarray().subspan(start, len));
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicBooleanArray
+ * Signature: (I[Z)Ledu/wpi/first/networktables/TimestampedBooleanArray;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicBooleanArray
+  (JNIEnv* env, jclass, jint subentry, jbooleanArray defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicBooleanArray(subentry, FromJavaBooleanArray(env, defaultValue)));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueBooleanArray
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedBooleanArray;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueBooleanArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueBooleanArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesBooleanArray
+ * Signature: (I)[[Z
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesBooleanArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObjectArray(env, nt::ReadQueueValuesBooleanArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setBooleanArray
+ * Signature: (IJ[Z)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setBooleanArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jbooleanArray value)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  return nt::SetBooleanArray(entry, FromJavaBooleanArray(env, value), time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getBooleanArray
+ * Signature: (I[Z)[Z
+ */
+JNIEXPORT jbooleanArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getBooleanArray
+  (JNIEnv* env, jclass, jint entry, jbooleanArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val.IsBooleanArray()) {
+    return defaultValue;
+  }
+  return MakeJBooleanArray(env, val.GetBooleanArray());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultBooleanArray
+ * Signature: (IJ[Z)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultBooleanArray
+  (JNIEnv* env, jclass, jint entry, jlong, jbooleanArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  return nt::SetDefaultBooleanArray(entry, FromJavaBooleanArray(env, defaultValue));
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicIntegerArray
+ * Signature: (I[J)Ledu/wpi/first/networktables/TimestampedIntegerArray;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicIntegerArray
+  (JNIEnv* env, jclass, jint subentry, jlongArray defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicIntegerArray(subentry, CriticalJSpan<const jlong>{env, defaultValue}));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueIntegerArray
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedIntegerArray;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueIntegerArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueIntegerArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesIntegerArray
+ * Signature: (I)[[J
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesIntegerArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObjectArray(env, nt::ReadQueueValuesIntegerArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setIntegerArray
+ * Signature: (IJ[J)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setIntegerArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jlongArray value)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  return nt::SetIntegerArray(entry, CriticalJSpan<const jlong>{env, value}, time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getIntegerArray
+ * Signature: (I[J)[J
+ */
+JNIEXPORT jlongArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getIntegerArray
+  (JNIEnv* env, jclass, jint entry, jlongArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val.IsIntegerArray()) {
+    return defaultValue;
+  }
+  return MakeJLongArray(env, val.GetIntegerArray());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultIntegerArray
+ * Signature: (IJ[J)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultIntegerArray
+  (JNIEnv* env, jclass, jint entry, jlong, jlongArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  return nt::SetDefaultIntegerArray(entry, CriticalJSpan<const jlong>{env, defaultValue});
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicFloatArray
+ * Signature: (I[F)Ledu/wpi/first/networktables/TimestampedFloatArray;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicFloatArray
+  (JNIEnv* env, jclass, jint subentry, jfloatArray defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicFloatArray(subentry, CriticalJSpan<const jfloat>{env, defaultValue}));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueFloatArray
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedFloatArray;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueFloatArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueFloatArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesFloatArray
+ * Signature: (I)[[F
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesFloatArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObjectArray(env, nt::ReadQueueValuesFloatArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setFloatArray
+ * Signature: (IJ[F)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setFloatArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jfloatArray value)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  return nt::SetFloatArray(entry, CriticalJSpan<const jfloat>{env, value}, time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getFloatArray
+ * Signature: (I[F)[F
+ */
+JNIEXPORT jfloatArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getFloatArray
+  (JNIEnv* env, jclass, jint entry, jfloatArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val.IsFloatArray()) {
+    return defaultValue;
+  }
+  return MakeJFloatArray(env, val.GetFloatArray());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultFloatArray
+ * Signature: (IJ[F)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultFloatArray
+  (JNIEnv* env, jclass, jint entry, jlong, jfloatArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  return nt::SetDefaultFloatArray(entry, CriticalJSpan<const jfloat>{env, defaultValue});
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicDoubleArray
+ * Signature: (I[D)Ledu/wpi/first/networktables/TimestampedDoubleArray;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicDoubleArray
+  (JNIEnv* env, jclass, jint subentry, jdoubleArray defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicDoubleArray(subentry, CriticalJSpan<const jdouble>{env, defaultValue}));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueDoubleArray
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedDoubleArray;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueDoubleArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueDoubleArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesDoubleArray
+ * Signature: (I)[[D
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesDoubleArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObjectArray(env, nt::ReadQueueValuesDoubleArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDoubleArray
+ * Signature: (IJ[D)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDoubleArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jdoubleArray value)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  return nt::SetDoubleArray(entry, CriticalJSpan<const jdouble>{env, value}, time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getDoubleArray
+ * Signature: (I[D)[D
+ */
+JNIEXPORT jdoubleArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getDoubleArray
+  (JNIEnv* env, jclass, jint entry, jdoubleArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val.IsDoubleArray()) {
+    return defaultValue;
+  }
+  return MakeJDoubleArray(env, val.GetDoubleArray());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultDoubleArray
+ * Signature: (IJ[D)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultDoubleArray
+  (JNIEnv* env, jclass, jint entry, jlong, jdoubleArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  return nt::SetDefaultDoubleArray(entry, CriticalJSpan<const jdouble>{env, defaultValue});
+}
+
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getAtomicStringArray
+ * Signature: (I[Ljava/lang/Object;)Ledu/wpi/first/networktables/TimestampedStringArray;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomicStringArray
+  (JNIEnv* env, jclass, jint subentry, jobjectArray defaultValue)
+{
+  return MakeJObject(env, nt::GetAtomicStringArray(subentry, FromJavaStringArray(env, defaultValue)));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueStringArray
+ * Signature: (I)[Ledu/wpi/first/networktables/TimestampedStringArray;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueStringArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObject(env, nt::ReadQueueStringArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    readQueueValuesStringArray
+ * Signature: (I)[[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValuesStringArray
+  (JNIEnv* env, jclass, jint subentry)
+{
+  return MakeJObjectArray(env, nt::ReadQueueValuesStringArray(subentry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setStringArray
+ * Signature: (IJ[Ljava/lang/Object;)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setStringArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jobjectArray value)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  return nt::SetStringArray(entry, FromJavaStringArray(env, value), time);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getStringArray
+ * Signature: (I[Ljava/lang/Object;)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getStringArray
+  (JNIEnv* env, jclass, jint entry, jobjectArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val.IsStringArray()) {
+    return defaultValue;
+  }
+  return MakeJStringArray(env, val.GetStringArray());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultStringArray
+ * Signature: (IJ[Ljava/lang/Object;)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultStringArray
+  (JNIEnv* env, jclass, jint entry, jlong, jobjectArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  return nt::SetDefaultStringArray(entry, FromJavaStringArray(env, defaultValue));
+}
+
+
+}  // extern "C"
diff --git a/ntcore/src/generated/main/native/cpp/ntcore_c_types.cpp b/ntcore/src/generated/main/native/cpp/ntcore_c_types.cpp
new file mode 100644
index 0000000..ca14c7d
--- /dev/null
+++ b/ntcore/src/generated/main/native/cpp/ntcore_c_types.cpp
@@ -0,0 +1,495 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#include "ntcore_c_types.h"
+
+#include "Value_internal.h"
+#include "ntcore_cpp.h"
+
+using namespace nt;
+
+template <typename T>
+static inline std::span<const T> ConvertFromC(const T* arr, size_t size) {
+  return {arr, size};
+}
+
+static inline std::string_view ConvertFromC(const char* arr, size_t size) {
+  return {arr, size};
+}
+
+static std::vector<std::string> ConvertFromC(const NT_String* arr, size_t size) {
+  std::vector<std::string> v;
+  v.reserve(size);
+  for (size_t i = 0; i < size; ++i) {
+    v.emplace_back(ConvertFromC(arr[i]));
+  }
+  return v;
+}
+
+
+static void ConvertToC(const nt::TimestampedBoolean& in, NT_TimestampedBoolean* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = in.value;
+}
+
+static void ConvertToC(const nt::TimestampedInteger& in, NT_TimestampedInteger* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = in.value;
+}
+
+static void ConvertToC(const nt::TimestampedFloat& in, NT_TimestampedFloat* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = in.value;
+}
+
+static void ConvertToC(const nt::TimestampedDouble& in, NT_TimestampedDouble* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = in.value;
+}
+
+static void ConvertToC(const nt::TimestampedString& in, NT_TimestampedString* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = ConvertToC<char>(in.value, &out->len);
+}
+
+static void ConvertToC(const nt::TimestampedRaw& in, NT_TimestampedRaw* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = ConvertToC<uint8_t>(in.value, &out->len);
+}
+
+static void ConvertToC(const nt::TimestampedBooleanArray& in, NT_TimestampedBooleanArray* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = ConvertToC<NT_Bool>(in.value, &out->len);
+}
+
+static void ConvertToC(const nt::TimestampedIntegerArray& in, NT_TimestampedIntegerArray* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = ConvertToC<int64_t>(in.value, &out->len);
+}
+
+static void ConvertToC(const nt::TimestampedFloatArray& in, NT_TimestampedFloatArray* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = ConvertToC<float>(in.value, &out->len);
+}
+
+static void ConvertToC(const nt::TimestampedDoubleArray& in, NT_TimestampedDoubleArray* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = ConvertToC<double>(in.value, &out->len);
+}
+
+static void ConvertToC(const nt::TimestampedStringArray& in, NT_TimestampedStringArray* out) {
+  out->time = in.time;
+  out->serverTime = in.serverTime;
+  out->value = ConvertToC<struct NT_String>(in.value, &out->len);
+}
+
+
+extern "C" {
+
+NT_Bool NT_SetBoolean(NT_Handle pubentry, int64_t time, NT_Bool value) {
+  return nt::SetBoolean(pubentry, value, time);
+}
+
+NT_Bool NT_SetDefaultBoolean(NT_Handle pubentry, NT_Bool defaultValue) {
+  return nt::SetDefaultBoolean(pubentry, defaultValue);
+}
+
+NT_Bool NT_GetBoolean(NT_Handle subentry, NT_Bool defaultValue) {
+  return nt::GetBoolean(subentry, defaultValue);
+}
+
+void NT_GetAtomicBoolean(NT_Handle subentry, NT_Bool defaultValue, struct NT_TimestampedBoolean* value) {
+  auto cppValue = nt::GetAtomicBoolean(subentry, defaultValue);
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedBoolean(struct NT_TimestampedBoolean* value) {
+}
+
+struct NT_TimestampedBoolean* NT_ReadQueueBoolean(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueBoolean(subentry);
+  return ConvertToC<NT_TimestampedBoolean>(arr, len);
+}
+
+void NT_FreeQueueBoolean(struct NT_TimestampedBoolean* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedBoolean(&arr[i]);
+  }
+  std::free(arr);
+}
+NT_Bool* NT_ReadQueueValuesBoolean(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueValuesBoolean(subentry);
+  return ConvertToC<NT_Bool>(arr, len);
+}
+
+
+NT_Bool NT_SetInteger(NT_Handle pubentry, int64_t time, int64_t value) {
+  return nt::SetInteger(pubentry, value, time);
+}
+
+NT_Bool NT_SetDefaultInteger(NT_Handle pubentry, int64_t defaultValue) {
+  return nt::SetDefaultInteger(pubentry, defaultValue);
+}
+
+int64_t NT_GetInteger(NT_Handle subentry, int64_t defaultValue) {
+  return nt::GetInteger(subentry, defaultValue);
+}
+
+void NT_GetAtomicInteger(NT_Handle subentry, int64_t defaultValue, struct NT_TimestampedInteger* value) {
+  auto cppValue = nt::GetAtomicInteger(subentry, defaultValue);
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedInteger(struct NT_TimestampedInteger* value) {
+}
+
+struct NT_TimestampedInteger* NT_ReadQueueInteger(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueInteger(subentry);
+  return ConvertToC<NT_TimestampedInteger>(arr, len);
+}
+
+void NT_FreeQueueInteger(struct NT_TimestampedInteger* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedInteger(&arr[i]);
+  }
+  std::free(arr);
+}
+int64_t* NT_ReadQueueValuesInteger(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueValuesInteger(subentry);
+  return ConvertToC<int64_t>(arr, len);
+}
+
+
+NT_Bool NT_SetFloat(NT_Handle pubentry, int64_t time, float value) {
+  return nt::SetFloat(pubentry, value, time);
+}
+
+NT_Bool NT_SetDefaultFloat(NT_Handle pubentry, float defaultValue) {
+  return nt::SetDefaultFloat(pubentry, defaultValue);
+}
+
+float NT_GetFloat(NT_Handle subentry, float defaultValue) {
+  return nt::GetFloat(subentry, defaultValue);
+}
+
+void NT_GetAtomicFloat(NT_Handle subentry, float defaultValue, struct NT_TimestampedFloat* value) {
+  auto cppValue = nt::GetAtomicFloat(subentry, defaultValue);
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedFloat(struct NT_TimestampedFloat* value) {
+}
+
+struct NT_TimestampedFloat* NT_ReadQueueFloat(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueFloat(subentry);
+  return ConvertToC<NT_TimestampedFloat>(arr, len);
+}
+
+void NT_FreeQueueFloat(struct NT_TimestampedFloat* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedFloat(&arr[i]);
+  }
+  std::free(arr);
+}
+float* NT_ReadQueueValuesFloat(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueValuesFloat(subentry);
+  return ConvertToC<float>(arr, len);
+}
+
+
+NT_Bool NT_SetDouble(NT_Handle pubentry, int64_t time, double value) {
+  return nt::SetDouble(pubentry, value, time);
+}
+
+NT_Bool NT_SetDefaultDouble(NT_Handle pubentry, double defaultValue) {
+  return nt::SetDefaultDouble(pubentry, defaultValue);
+}
+
+double NT_GetDouble(NT_Handle subentry, double defaultValue) {
+  return nt::GetDouble(subentry, defaultValue);
+}
+
+void NT_GetAtomicDouble(NT_Handle subentry, double defaultValue, struct NT_TimestampedDouble* value) {
+  auto cppValue = nt::GetAtomicDouble(subentry, defaultValue);
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedDouble(struct NT_TimestampedDouble* value) {
+}
+
+struct NT_TimestampedDouble* NT_ReadQueueDouble(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueDouble(subentry);
+  return ConvertToC<NT_TimestampedDouble>(arr, len);
+}
+
+void NT_FreeQueueDouble(struct NT_TimestampedDouble* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedDouble(&arr[i]);
+  }
+  std::free(arr);
+}
+double* NT_ReadQueueValuesDouble(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueValuesDouble(subentry);
+  return ConvertToC<double>(arr, len);
+}
+
+
+NT_Bool NT_SetString(NT_Handle pubentry, int64_t time, const char* value, size_t len) {
+  return nt::SetString(pubentry, ConvertFromC(value, len), time);
+}
+
+NT_Bool NT_SetDefaultString(NT_Handle pubentry, const char* defaultValue, size_t defaultValueLen) {
+  return nt::SetDefaultString(pubentry, ConvertFromC(defaultValue, defaultValueLen));
+}
+
+char* NT_GetString(NT_Handle subentry, const char* defaultValue, size_t defaultValueLen, size_t* len) {
+  auto cppValue = nt::GetString(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  return ConvertToC<char>(cppValue, len);
+}
+
+void NT_GetAtomicString(NT_Handle subentry, const char* defaultValue, size_t defaultValueLen, struct NT_TimestampedString* value) {
+  auto cppValue = nt::GetAtomicString(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedString(struct NT_TimestampedString* value) {
+  std::free(value->value);
+}
+
+struct NT_TimestampedString* NT_ReadQueueString(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueString(subentry);
+  return ConvertToC<NT_TimestampedString>(arr, len);
+}
+
+void NT_FreeQueueString(struct NT_TimestampedString* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedString(&arr[i]);
+  }
+  std::free(arr);
+}
+
+
+NT_Bool NT_SetRaw(NT_Handle pubentry, int64_t time, const uint8_t* value, size_t len) {
+  return nt::SetRaw(pubentry, ConvertFromC(value, len), time);
+}
+
+NT_Bool NT_SetDefaultRaw(NT_Handle pubentry, const uint8_t* defaultValue, size_t defaultValueLen) {
+  return nt::SetDefaultRaw(pubentry, ConvertFromC(defaultValue, defaultValueLen));
+}
+
+uint8_t* NT_GetRaw(NT_Handle subentry, const uint8_t* defaultValue, size_t defaultValueLen, size_t* len) {
+  auto cppValue = nt::GetRaw(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  return ConvertToC<uint8_t>(cppValue, len);
+}
+
+void NT_GetAtomicRaw(NT_Handle subentry, const uint8_t* defaultValue, size_t defaultValueLen, struct NT_TimestampedRaw* value) {
+  auto cppValue = nt::GetAtomicRaw(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedRaw(struct NT_TimestampedRaw* value) {
+  std::free(value->value);
+}
+
+struct NT_TimestampedRaw* NT_ReadQueueRaw(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueRaw(subentry);
+  return ConvertToC<NT_TimestampedRaw>(arr, len);
+}
+
+void NT_FreeQueueRaw(struct NT_TimestampedRaw* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedRaw(&arr[i]);
+  }
+  std::free(arr);
+}
+
+
+NT_Bool NT_SetBooleanArray(NT_Handle pubentry, int64_t time, const NT_Bool* value, size_t len) {
+  return nt::SetBooleanArray(pubentry, ConvertFromC(value, len), time);
+}
+
+NT_Bool NT_SetDefaultBooleanArray(NT_Handle pubentry, const NT_Bool* defaultValue, size_t defaultValueLen) {
+  return nt::SetDefaultBooleanArray(pubentry, ConvertFromC(defaultValue, defaultValueLen));
+}
+
+NT_Bool* NT_GetBooleanArray(NT_Handle subentry, const NT_Bool* defaultValue, size_t defaultValueLen, size_t* len) {
+  auto cppValue = nt::GetBooleanArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  return ConvertToC<NT_Bool>(cppValue, len);
+}
+
+void NT_GetAtomicBooleanArray(NT_Handle subentry, const NT_Bool* defaultValue, size_t defaultValueLen, struct NT_TimestampedBooleanArray* value) {
+  auto cppValue = nt::GetAtomicBooleanArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedBooleanArray(struct NT_TimestampedBooleanArray* value) {
+  std::free(value->value);
+}
+
+struct NT_TimestampedBooleanArray* NT_ReadQueueBooleanArray(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueBooleanArray(subentry);
+  return ConvertToC<NT_TimestampedBooleanArray>(arr, len);
+}
+
+void NT_FreeQueueBooleanArray(struct NT_TimestampedBooleanArray* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedBooleanArray(&arr[i]);
+  }
+  std::free(arr);
+}
+
+
+NT_Bool NT_SetIntegerArray(NT_Handle pubentry, int64_t time, const int64_t* value, size_t len) {
+  return nt::SetIntegerArray(pubentry, ConvertFromC(value, len), time);
+}
+
+NT_Bool NT_SetDefaultIntegerArray(NT_Handle pubentry, const int64_t* defaultValue, size_t defaultValueLen) {
+  return nt::SetDefaultIntegerArray(pubentry, ConvertFromC(defaultValue, defaultValueLen));
+}
+
+int64_t* NT_GetIntegerArray(NT_Handle subentry, const int64_t* defaultValue, size_t defaultValueLen, size_t* len) {
+  auto cppValue = nt::GetIntegerArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  return ConvertToC<int64_t>(cppValue, len);
+}
+
+void NT_GetAtomicIntegerArray(NT_Handle subentry, const int64_t* defaultValue, size_t defaultValueLen, struct NT_TimestampedIntegerArray* value) {
+  auto cppValue = nt::GetAtomicIntegerArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedIntegerArray(struct NT_TimestampedIntegerArray* value) {
+  std::free(value->value);
+}
+
+struct NT_TimestampedIntegerArray* NT_ReadQueueIntegerArray(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueIntegerArray(subentry);
+  return ConvertToC<NT_TimestampedIntegerArray>(arr, len);
+}
+
+void NT_FreeQueueIntegerArray(struct NT_TimestampedIntegerArray* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedIntegerArray(&arr[i]);
+  }
+  std::free(arr);
+}
+
+
+NT_Bool NT_SetFloatArray(NT_Handle pubentry, int64_t time, const float* value, size_t len) {
+  return nt::SetFloatArray(pubentry, ConvertFromC(value, len), time);
+}
+
+NT_Bool NT_SetDefaultFloatArray(NT_Handle pubentry, const float* defaultValue, size_t defaultValueLen) {
+  return nt::SetDefaultFloatArray(pubentry, ConvertFromC(defaultValue, defaultValueLen));
+}
+
+float* NT_GetFloatArray(NT_Handle subentry, const float* defaultValue, size_t defaultValueLen, size_t* len) {
+  auto cppValue = nt::GetFloatArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  return ConvertToC<float>(cppValue, len);
+}
+
+void NT_GetAtomicFloatArray(NT_Handle subentry, const float* defaultValue, size_t defaultValueLen, struct NT_TimestampedFloatArray* value) {
+  auto cppValue = nt::GetAtomicFloatArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedFloatArray(struct NT_TimestampedFloatArray* value) {
+  std::free(value->value);
+}
+
+struct NT_TimestampedFloatArray* NT_ReadQueueFloatArray(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueFloatArray(subentry);
+  return ConvertToC<NT_TimestampedFloatArray>(arr, len);
+}
+
+void NT_FreeQueueFloatArray(struct NT_TimestampedFloatArray* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedFloatArray(&arr[i]);
+  }
+  std::free(arr);
+}
+
+
+NT_Bool NT_SetDoubleArray(NT_Handle pubentry, int64_t time, const double* value, size_t len) {
+  return nt::SetDoubleArray(pubentry, ConvertFromC(value, len), time);
+}
+
+NT_Bool NT_SetDefaultDoubleArray(NT_Handle pubentry, const double* defaultValue, size_t defaultValueLen) {
+  return nt::SetDefaultDoubleArray(pubentry, ConvertFromC(defaultValue, defaultValueLen));
+}
+
+double* NT_GetDoubleArray(NT_Handle subentry, const double* defaultValue, size_t defaultValueLen, size_t* len) {
+  auto cppValue = nt::GetDoubleArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  return ConvertToC<double>(cppValue, len);
+}
+
+void NT_GetAtomicDoubleArray(NT_Handle subentry, const double* defaultValue, size_t defaultValueLen, struct NT_TimestampedDoubleArray* value) {
+  auto cppValue = nt::GetAtomicDoubleArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedDoubleArray(struct NT_TimestampedDoubleArray* value) {
+  std::free(value->value);
+}
+
+struct NT_TimestampedDoubleArray* NT_ReadQueueDoubleArray(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueDoubleArray(subentry);
+  return ConvertToC<NT_TimestampedDoubleArray>(arr, len);
+}
+
+void NT_FreeQueueDoubleArray(struct NT_TimestampedDoubleArray* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedDoubleArray(&arr[i]);
+  }
+  std::free(arr);
+}
+
+
+NT_Bool NT_SetStringArray(NT_Handle pubentry, int64_t time, const struct NT_String* value, size_t len) {
+  return nt::SetStringArray(pubentry, ConvertFromC(value, len), time);
+}
+
+NT_Bool NT_SetDefaultStringArray(NT_Handle pubentry, const struct NT_String* defaultValue, size_t defaultValueLen) {
+  return nt::SetDefaultStringArray(pubentry, ConvertFromC(defaultValue, defaultValueLen));
+}
+
+struct NT_String* NT_GetStringArray(NT_Handle subentry, const struct NT_String* defaultValue, size_t defaultValueLen, size_t* len) {
+  auto cppValue = nt::GetStringArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  return ConvertToC<struct NT_String>(cppValue, len);
+}
+
+void NT_GetAtomicStringArray(NT_Handle subentry, const struct NT_String* defaultValue, size_t defaultValueLen, struct NT_TimestampedStringArray* value) {
+  auto cppValue = nt::GetAtomicStringArray(subentry, ConvertFromC(defaultValue, defaultValueLen));
+  ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestampedStringArray(struct NT_TimestampedStringArray* value) {
+  NT_FreeStringArray(value->value, value->len);
+}
+
+struct NT_TimestampedStringArray* NT_ReadQueueStringArray(NT_Handle subentry, size_t* len) {
+  auto arr = nt::ReadQueueStringArray(subentry);
+  return ConvertToC<NT_TimestampedStringArray>(arr, len);
+}
+
+void NT_FreeQueueStringArray(struct NT_TimestampedStringArray* arr, size_t len) {
+  for (size_t i = 0; i < len; ++i) {
+    NT_DisposeTimestampedStringArray(&arr[i]);
+  }
+  std::free(arr);
+}
+
+
+}  // extern "C"
diff --git a/ntcore/src/generated/main/native/cpp/ntcore_cpp_types.cpp b/ntcore/src/generated/main/native/cpp/ntcore_cpp_types.cpp
new file mode 100644
index 0000000..030857e
--- /dev/null
+++ b/ntcore/src/generated/main/native/cpp/ntcore_cpp_types.cpp
@@ -0,0 +1,463 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#include "ntcore_cpp_types.h"
+
+#include "Handle.h"
+#include "InstanceImpl.h"
+
+namespace {
+template <nt::ValidType T>
+struct ValuesType {
+  using Vector =
+      std::vector<typename nt::TypeInfo<std::remove_cvref_t<T>>::Value>;
+};
+
+template <>
+struct ValuesType<bool> {
+  using Vector = std::vector<int>;
+};
+}  // namespace
+
+namespace nt {
+
+template <ValidType T>
+static inline bool Set(NT_Handle pubentry, typename TypeInfo<T>::View value,
+                       int64_t time) {
+  if (auto ii = InstanceImpl::Get(Handle{pubentry}.GetInst())) {
+    return ii->localStorage.SetEntryValue(
+        pubentry, MakeValue<T>(value, time == 0 ? Now() : time));
+  } else {
+    return {};
+  }
+}
+
+template <ValidType T>
+static inline bool SetDefault(NT_Handle pubentry,
+                              typename TypeInfo<T>::View defaultValue) {
+  if (auto ii = InstanceImpl::Get(Handle{pubentry}.GetInst())) {
+    return ii->localStorage.SetDefaultEntryValue(pubentry,
+                                                 MakeValue<T>(defaultValue, 1));
+  } else {
+    return {};
+  }
+}
+
+template <ValidType T>
+static inline Timestamped<typename TypeInfo<T>::Value> GetAtomic(
+    NT_Handle subentry, typename TypeInfo<T>::View defaultValue) {
+  if (auto ii = InstanceImpl::Get(Handle{subentry}.GetInst())) {
+    return ii->localStorage.GetAtomic<T>(subentry, defaultValue);
+  } else {
+    return {};
+  }
+}
+
+template <ValidType T>
+inline Timestamped<typename TypeInfo<T>::SmallRet> GetAtomic(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<typename TypeInfo<T>::SmallElem>& buf,
+    typename TypeInfo<T>::View defaultValue) {
+  if (auto ii = InstanceImpl::Get(Handle{subentry}.GetInst())) {
+    return ii->localStorage.GetAtomic<T>(subentry, buf, defaultValue);
+  } else {
+    return {};
+  }
+}
+
+template <typename T>
+static inline std::vector<Timestamped<typename TypeInfo<T>::Value>> ReadQueue(
+    NT_Handle subentry) {
+  if (auto ii = InstanceImpl::Get(Handle{subentry}.GetInst())) {
+    return ii->localStorage.ReadQueue<T>(subentry);
+  } else {
+    return {};
+  }
+}
+
+template <typename T>
+static inline typename ValuesType<T>::Vector ReadQueueValues(
+    NT_Handle subentry) {
+  typename ValuesType<T>::Vector rv;
+  auto arr = ReadQueue<T>(subentry);
+  rv.reserve(arr.size());
+  for (auto&& elem : arr) {
+    rv.emplace_back(std::move(elem.value));
+  }
+  return rv;
+}
+
+bool SetBoolean(NT_Handle pubentry, bool value, int64_t time) {
+  return Set<bool>(pubentry, value, time);
+}
+
+bool SetDefaultBoolean(NT_Handle pubentry, bool defaultValue) {
+  return SetDefault<bool>(pubentry, defaultValue);
+}
+
+bool GetBoolean(NT_Handle subentry, bool defaultValue) {
+  return GetAtomic<bool>(subentry, defaultValue).value;
+}
+
+TimestampedBoolean GetAtomicBoolean(
+    NT_Handle subentry, bool defaultValue) {
+  return GetAtomic<bool>(subentry, defaultValue);
+}
+
+std::vector<TimestampedBoolean> ReadQueueBoolean(NT_Handle subentry) {
+  return ReadQueue<bool>(subentry);
+}
+
+std::vector<int> ReadQueueValuesBoolean(NT_Handle subentry) {
+  return ReadQueueValues<bool>(subentry);
+}
+
+
+bool SetInteger(NT_Handle pubentry, int64_t value, int64_t time) {
+  return Set<int64_t>(pubentry, value, time);
+}
+
+bool SetDefaultInteger(NT_Handle pubentry, int64_t defaultValue) {
+  return SetDefault<int64_t>(pubentry, defaultValue);
+}
+
+int64_t GetInteger(NT_Handle subentry, int64_t defaultValue) {
+  return GetAtomic<int64_t>(subentry, defaultValue).value;
+}
+
+TimestampedInteger GetAtomicInteger(
+    NT_Handle subentry, int64_t defaultValue) {
+  return GetAtomic<int64_t>(subentry, defaultValue);
+}
+
+std::vector<TimestampedInteger> ReadQueueInteger(NT_Handle subentry) {
+  return ReadQueue<int64_t>(subentry);
+}
+
+std::vector<int64_t> ReadQueueValuesInteger(NT_Handle subentry) {
+  return ReadQueueValues<int64_t>(subentry);
+}
+
+
+bool SetFloat(NT_Handle pubentry, float value, int64_t time) {
+  return Set<float>(pubentry, value, time);
+}
+
+bool SetDefaultFloat(NT_Handle pubentry, float defaultValue) {
+  return SetDefault<float>(pubentry, defaultValue);
+}
+
+float GetFloat(NT_Handle subentry, float defaultValue) {
+  return GetAtomic<float>(subentry, defaultValue).value;
+}
+
+TimestampedFloat GetAtomicFloat(
+    NT_Handle subentry, float defaultValue) {
+  return GetAtomic<float>(subentry, defaultValue);
+}
+
+std::vector<TimestampedFloat> ReadQueueFloat(NT_Handle subentry) {
+  return ReadQueue<float>(subentry);
+}
+
+std::vector<float> ReadQueueValuesFloat(NT_Handle subentry) {
+  return ReadQueueValues<float>(subentry);
+}
+
+
+bool SetDouble(NT_Handle pubentry, double value, int64_t time) {
+  return Set<double>(pubentry, value, time);
+}
+
+bool SetDefaultDouble(NT_Handle pubentry, double defaultValue) {
+  return SetDefault<double>(pubentry, defaultValue);
+}
+
+double GetDouble(NT_Handle subentry, double defaultValue) {
+  return GetAtomic<double>(subentry, defaultValue).value;
+}
+
+TimestampedDouble GetAtomicDouble(
+    NT_Handle subentry, double defaultValue) {
+  return GetAtomic<double>(subentry, defaultValue);
+}
+
+std::vector<TimestampedDouble> ReadQueueDouble(NT_Handle subentry) {
+  return ReadQueue<double>(subentry);
+}
+
+std::vector<double> ReadQueueValuesDouble(NT_Handle subentry) {
+  return ReadQueueValues<double>(subentry);
+}
+
+
+bool SetString(NT_Handle pubentry, std::string_view value, int64_t time) {
+  return Set<std::string>(pubentry, value, time);
+}
+
+bool SetDefaultString(NT_Handle pubentry, std::string_view defaultValue) {
+  return SetDefault<std::string>(pubentry, defaultValue);
+}
+
+std::string GetString(NT_Handle subentry, std::string_view defaultValue) {
+  return GetAtomic<std::string>(subentry, defaultValue).value;
+}
+
+TimestampedString GetAtomicString(
+    NT_Handle subentry, std::string_view defaultValue) {
+  return GetAtomic<std::string>(subentry, defaultValue);
+}
+
+std::vector<TimestampedString> ReadQueueString(NT_Handle subentry) {
+  return ReadQueue<std::string>(subentry);
+}
+
+std::vector<std::string> ReadQueueValuesString(NT_Handle subentry) {
+  return ReadQueueValues<std::string>(subentry);
+}
+
+std::string_view GetString(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<char>& buf,
+    std::string_view defaultValue) {
+  return GetAtomic<std::string>(subentry, buf, defaultValue).value;
+}
+
+TimestampedStringView GetAtomicString(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<char>& buf,
+    std::string_view defaultValue) {
+  return GetAtomic<std::string>(subentry, buf, defaultValue);
+}
+
+
+bool SetRaw(NT_Handle pubentry, std::span<const uint8_t> value, int64_t time) {
+  return Set<uint8_t[]>(pubentry, value, time);
+}
+
+bool SetDefaultRaw(NT_Handle pubentry, std::span<const uint8_t> defaultValue) {
+  return SetDefault<uint8_t[]>(pubentry, defaultValue);
+}
+
+std::vector<uint8_t> GetRaw(NT_Handle subentry, std::span<const uint8_t> defaultValue) {
+  return GetAtomic<uint8_t[]>(subentry, defaultValue).value;
+}
+
+TimestampedRaw GetAtomicRaw(
+    NT_Handle subentry, std::span<const uint8_t> defaultValue) {
+  return GetAtomic<uint8_t[]>(subentry, defaultValue);
+}
+
+std::vector<TimestampedRaw> ReadQueueRaw(NT_Handle subentry) {
+  return ReadQueue<uint8_t[]>(subentry);
+}
+
+std::vector<std::vector<uint8_t>> ReadQueueValuesRaw(NT_Handle subentry) {
+  return ReadQueueValues<uint8_t[]>(subentry);
+}
+
+std::span<uint8_t> GetRaw(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<uint8_t>& buf,
+    std::span<const uint8_t> defaultValue) {
+  return GetAtomic<uint8_t[]>(subentry, buf, defaultValue).value;
+}
+
+TimestampedRawView GetAtomicRaw(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<uint8_t>& buf,
+    std::span<const uint8_t> defaultValue) {
+  return GetAtomic<uint8_t[]>(subentry, buf, defaultValue);
+}
+
+
+bool SetBooleanArray(NT_Handle pubentry, std::span<const int> value, int64_t time) {
+  return Set<bool[]>(pubentry, value, time);
+}
+
+bool SetDefaultBooleanArray(NT_Handle pubentry, std::span<const int> defaultValue) {
+  return SetDefault<bool[]>(pubentry, defaultValue);
+}
+
+std::vector<int> GetBooleanArray(NT_Handle subentry, std::span<const int> defaultValue) {
+  return GetAtomic<bool[]>(subentry, defaultValue).value;
+}
+
+TimestampedBooleanArray GetAtomicBooleanArray(
+    NT_Handle subentry, std::span<const int> defaultValue) {
+  return GetAtomic<bool[]>(subentry, defaultValue);
+}
+
+std::vector<TimestampedBooleanArray> ReadQueueBooleanArray(NT_Handle subentry) {
+  return ReadQueue<bool[]>(subentry);
+}
+
+std::vector<std::vector<int>> ReadQueueValuesBooleanArray(NT_Handle subentry) {
+  return ReadQueueValues<bool[]>(subentry);
+}
+
+std::span<int> GetBooleanArray(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<int>& buf,
+    std::span<const int> defaultValue) {
+  return GetAtomic<bool[]>(subentry, buf, defaultValue).value;
+}
+
+TimestampedBooleanArrayView GetAtomicBooleanArray(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<int>& buf,
+    std::span<const int> defaultValue) {
+  return GetAtomic<bool[]>(subentry, buf, defaultValue);
+}
+
+
+bool SetIntegerArray(NT_Handle pubentry, std::span<const int64_t> value, int64_t time) {
+  return Set<int64_t[]>(pubentry, value, time);
+}
+
+bool SetDefaultIntegerArray(NT_Handle pubentry, std::span<const int64_t> defaultValue) {
+  return SetDefault<int64_t[]>(pubentry, defaultValue);
+}
+
+std::vector<int64_t> GetIntegerArray(NT_Handle subentry, std::span<const int64_t> defaultValue) {
+  return GetAtomic<int64_t[]>(subentry, defaultValue).value;
+}
+
+TimestampedIntegerArray GetAtomicIntegerArray(
+    NT_Handle subentry, std::span<const int64_t> defaultValue) {
+  return GetAtomic<int64_t[]>(subentry, defaultValue);
+}
+
+std::vector<TimestampedIntegerArray> ReadQueueIntegerArray(NT_Handle subentry) {
+  return ReadQueue<int64_t[]>(subentry);
+}
+
+std::vector<std::vector<int64_t>> ReadQueueValuesIntegerArray(NT_Handle subentry) {
+  return ReadQueueValues<int64_t[]>(subentry);
+}
+
+std::span<int64_t> GetIntegerArray(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<int64_t>& buf,
+    std::span<const int64_t> defaultValue) {
+  return GetAtomic<int64_t[]>(subentry, buf, defaultValue).value;
+}
+
+TimestampedIntegerArrayView GetAtomicIntegerArray(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<int64_t>& buf,
+    std::span<const int64_t> defaultValue) {
+  return GetAtomic<int64_t[]>(subentry, buf, defaultValue);
+}
+
+
+bool SetFloatArray(NT_Handle pubentry, std::span<const float> value, int64_t time) {
+  return Set<float[]>(pubentry, value, time);
+}
+
+bool SetDefaultFloatArray(NT_Handle pubentry, std::span<const float> defaultValue) {
+  return SetDefault<float[]>(pubentry, defaultValue);
+}
+
+std::vector<float> GetFloatArray(NT_Handle subentry, std::span<const float> defaultValue) {
+  return GetAtomic<float[]>(subentry, defaultValue).value;
+}
+
+TimestampedFloatArray GetAtomicFloatArray(
+    NT_Handle subentry, std::span<const float> defaultValue) {
+  return GetAtomic<float[]>(subentry, defaultValue);
+}
+
+std::vector<TimestampedFloatArray> ReadQueueFloatArray(NT_Handle subentry) {
+  return ReadQueue<float[]>(subentry);
+}
+
+std::vector<std::vector<float>> ReadQueueValuesFloatArray(NT_Handle subentry) {
+  return ReadQueueValues<float[]>(subentry);
+}
+
+std::span<float> GetFloatArray(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<float>& buf,
+    std::span<const float> defaultValue) {
+  return GetAtomic<float[]>(subentry, buf, defaultValue).value;
+}
+
+TimestampedFloatArrayView GetAtomicFloatArray(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<float>& buf,
+    std::span<const float> defaultValue) {
+  return GetAtomic<float[]>(subentry, buf, defaultValue);
+}
+
+
+bool SetDoubleArray(NT_Handle pubentry, std::span<const double> value, int64_t time) {
+  return Set<double[]>(pubentry, value, time);
+}
+
+bool SetDefaultDoubleArray(NT_Handle pubentry, std::span<const double> defaultValue) {
+  return SetDefault<double[]>(pubentry, defaultValue);
+}
+
+std::vector<double> GetDoubleArray(NT_Handle subentry, std::span<const double> defaultValue) {
+  return GetAtomic<double[]>(subentry, defaultValue).value;
+}
+
+TimestampedDoubleArray GetAtomicDoubleArray(
+    NT_Handle subentry, std::span<const double> defaultValue) {
+  return GetAtomic<double[]>(subentry, defaultValue);
+}
+
+std::vector<TimestampedDoubleArray> ReadQueueDoubleArray(NT_Handle subentry) {
+  return ReadQueue<double[]>(subentry);
+}
+
+std::vector<std::vector<double>> ReadQueueValuesDoubleArray(NT_Handle subentry) {
+  return ReadQueueValues<double[]>(subentry);
+}
+
+std::span<double> GetDoubleArray(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<double>& buf,
+    std::span<const double> defaultValue) {
+  return GetAtomic<double[]>(subentry, buf, defaultValue).value;
+}
+
+TimestampedDoubleArrayView GetAtomicDoubleArray(
+    NT_Handle subentry,
+    wpi::SmallVectorImpl<double>& buf,
+    std::span<const double> defaultValue) {
+  return GetAtomic<double[]>(subentry, buf, defaultValue);
+}
+
+
+bool SetStringArray(NT_Handle pubentry, std::span<const std::string> value, int64_t time) {
+  return Set<std::string[]>(pubentry, value, time);
+}
+
+bool SetDefaultStringArray(NT_Handle pubentry, std::span<const std::string> defaultValue) {
+  return SetDefault<std::string[]>(pubentry, defaultValue);
+}
+
+std::vector<std::string> GetStringArray(NT_Handle subentry, std::span<const std::string> defaultValue) {
+  return GetAtomic<std::string[]>(subentry, defaultValue).value;
+}
+
+TimestampedStringArray GetAtomicStringArray(
+    NT_Handle subentry, std::span<const std::string> defaultValue) {
+  return GetAtomic<std::string[]>(subentry, defaultValue);
+}
+
+std::vector<TimestampedStringArray> ReadQueueStringArray(NT_Handle subentry) {
+  return ReadQueue<std::string[]>(subentry);
+}
+
+std::vector<std::vector<std::string>> ReadQueueValuesStringArray(NT_Handle subentry) {
+  return ReadQueueValues<std::string[]>(subentry);
+}
+
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/BooleanArrayTopic.h
similarity index 74%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/BooleanArrayTopic.h
index ec2a915..de89f4b 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/BooleanArrayTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+#include <utility>
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,33 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class BooleanArrayTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables BooleanArray subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class BooleanArraySubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = BooleanArrayTopic;
+  using ValueType = std::vector<int>;
+  using ParamType = std::span<const int>;
+  using TimestampedValueType = TimestampedBooleanArray;
 
-  {{ TypeName }}Subscriber() = default;
+  using SmallRetType = std::span<int>;
+  using SmallElemType = int;
+  using TimestampedValueViewType = TimestampedBooleanArrayView;
+
+
+  BooleanArraySubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * BooleanArrayTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  BooleanArraySubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,7 +68,7 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value.
    * If no value has been published, returns the stored default value.
@@ -85,7 +87,7 @@
    * @return value
    */
   SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -104,7 +106,7 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value along with its timestamp.
    * If no value has been published, returns the stored default value and a
@@ -128,7 +130,7 @@
   TimestampedValueViewType GetAtomic(
       wpi::SmallVectorImpl<SmallElemType>& buf,
       ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
@@ -153,28 +155,28 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables BooleanArray publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class BooleanArrayPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = BooleanArrayTopic;
+  using ValueType = std::vector<int>;
+  using ParamType = std::span<const int>;
 
-  {{ TypeName }}Publisher() = default;
+  using SmallRetType = std::span<int>;
+  using SmallElemType = int;
+
+  using TimestampedValueType = TimestampedBooleanArray;
+
+  BooleanArrayPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * BooleanArrayTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit BooleanArrayPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +204,34 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables BooleanArray entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class BooleanArrayEntry final : public BooleanArraySubscriber,
+                                  public BooleanArrayPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = BooleanArraySubscriber;
+  using PublisherType = BooleanArrayPublisher;
+  using TopicType = BooleanArrayTopic;
+  using ValueType = std::vector<int>;
+  using ParamType = std::span<const int>;
 
-  {{ TypeName }}Entry() = default;
+  using SmallRetType = std::span<int>;
+  using SmallElemType = int;
+
+  using TimestampedValueType = TimestampedBooleanArray;
+
+  BooleanArrayEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * BooleanArrayTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  BooleanArrayEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +261,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables BooleanArray topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class BooleanArrayTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = BooleanArraySubscriber;
+  using PublisherType = BooleanArrayPublisher;
+  using EntryType = BooleanArrayEntry;
+  using ValueType = std::vector<int>;
+  using ParamType = std::span<const int>;
+  using TimestampedValueType = TimestampedBooleanArray;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "boolean[]";
 
-  {{ TypeName }}Topic() = default;
+  BooleanArrayTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetBooleanArrayTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit BooleanArrayTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit BooleanArrayTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +301,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +308,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +330,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +343,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +386,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +418,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/BooleanArrayTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/BooleanArrayTopic.inc b/ntcore/src/generated/main/native/include/networktables/BooleanArrayTopic.inc
new file mode 100644
index 0000000..404ec7e
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/BooleanArrayTopic.inc
@@ -0,0 +1,137 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/BooleanArrayTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline BooleanArraySubscriber::BooleanArraySubscriber(
+    NT_Subscriber handle, std::span<const int> defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue.begin(), defaultValue.end()} {}
+
+inline std::vector<int> BooleanArraySubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline std::vector<int> BooleanArraySubscriber::Get(
+    std::span<const int> defaultValue) const {
+  return ::nt::GetBooleanArray(m_subHandle, defaultValue);
+}
+
+inline std::span<int> BooleanArraySubscriber::Get(wpi::SmallVectorImpl<int>& buf) const {
+  return Get(buf, m_defaultValue);
+}
+
+inline std::span<int> BooleanArraySubscriber::Get(wpi::SmallVectorImpl<int>& buf, std::span<const int> defaultValue) const {
+  return nt::GetBooleanArray(m_subHandle, buf, defaultValue);
+}
+
+inline TimestampedBooleanArray BooleanArraySubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedBooleanArray BooleanArraySubscriber::GetAtomic(
+    std::span<const int> defaultValue) const {
+  return ::nt::GetAtomicBooleanArray(m_subHandle, defaultValue);
+}
+
+inline TimestampedBooleanArrayView BooleanArraySubscriber::GetAtomic(wpi::SmallVectorImpl<int>& buf) const {
+  return GetAtomic(buf, m_defaultValue);
+}
+
+inline TimestampedBooleanArrayView BooleanArraySubscriber::GetAtomic(wpi::SmallVectorImpl<int>& buf, std::span<const int> defaultValue) const {
+  return nt::GetAtomicBooleanArray(m_subHandle, buf, defaultValue);
+}
+
+inline std::vector<TimestampedBooleanArray>
+BooleanArraySubscriber::ReadQueue() {
+  return ::nt::ReadQueueBooleanArray(m_subHandle);
+}
+
+inline BooleanArrayTopic BooleanArraySubscriber::GetTopic() const {
+  return BooleanArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline BooleanArrayPublisher::BooleanArrayPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void BooleanArrayPublisher::Set(std::span<const int> value,
+                                         int64_t time) {
+  ::nt::SetBooleanArray(m_pubHandle, value, time);
+}
+
+inline void BooleanArrayPublisher::SetDefault(std::span<const int> value) {
+  ::nt::SetDefaultBooleanArray(m_pubHandle, value);
+}
+
+inline BooleanArrayTopic BooleanArrayPublisher::GetTopic() const {
+  return BooleanArrayTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline BooleanArrayEntry::BooleanArrayEntry(
+    NT_Entry handle, std::span<const int> defaultValue)
+    : BooleanArraySubscriber{handle, defaultValue},
+      BooleanArrayPublisher{handle} {}
+
+inline BooleanArrayTopic BooleanArrayEntry::GetTopic() const {
+  return BooleanArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void BooleanArrayEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline BooleanArraySubscriber BooleanArrayTopic::Subscribe(
+    std::span<const int> defaultValue,
+    const PubSubOptions& options) {
+  return BooleanArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_BOOLEAN_ARRAY, "boolean[]", options),
+      defaultValue};
+}
+inline BooleanArraySubscriber BooleanArrayTopic::SubscribeEx(
+    std::string_view typeString, std::span<const int> defaultValue,
+    const PubSubOptions& options) {
+  return BooleanArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_BOOLEAN_ARRAY, typeString, options),
+      defaultValue};
+}
+
+inline BooleanArrayPublisher BooleanArrayTopic::Publish(
+    const PubSubOptions& options) {
+  return BooleanArrayPublisher{
+      ::nt::Publish(m_handle, NT_BOOLEAN_ARRAY, "boolean[]", options)};
+}
+
+inline BooleanArrayPublisher BooleanArrayTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return BooleanArrayPublisher{
+      ::nt::PublishEx(m_handle, NT_BOOLEAN_ARRAY, typeString, properties, options)};
+}
+
+inline BooleanArrayEntry BooleanArrayTopic::GetEntry(
+    std::span<const int> defaultValue,
+    const PubSubOptions& options) {
+  return BooleanArrayEntry{
+      ::nt::GetEntry(m_handle, NT_BOOLEAN_ARRAY, "boolean[]", options),
+      defaultValue};
+}
+inline BooleanArrayEntry BooleanArrayTopic::GetEntryEx(
+    std::string_view typeString, std::span<const int> defaultValue,
+    const PubSubOptions& options) {
+  return BooleanArrayEntry{
+      ::nt::GetEntry(m_handle, NT_BOOLEAN_ARRAY, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/BooleanTopic.h
similarity index 64%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/BooleanTopic.h
index ec2a915..22db06b 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/BooleanTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,29 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class BooleanTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables Boolean subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class BooleanSubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = BooleanTopic;
+  using ValueType = bool;
+  using ParamType = bool;
+  using TimestampedValueType = TimestampedBoolean;
 
-  {{ TypeName }}Subscriber() = default;
+
+  BooleanSubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * BooleanTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  BooleanSubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,27 +64,8 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value.
-   * If no value has been published, returns the stored default value.
-   *
-   * @param buf storage for returned value
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value.
-   * If no value has been published, returns the passed defaultValue.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
    * timestamp of 0.
@@ -104,32 +83,8 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the stored default value and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the passed defaultValue and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf,
-      ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
    *
@@ -153,28 +108,25 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables Boolean publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class BooleanPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = BooleanTopic;
+  using ValueType = bool;
+  using ParamType = bool;
 
-  {{ TypeName }}Publisher() = default;
+  using TimestampedValueType = TimestampedBoolean;
+
+  BooleanPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * BooleanTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit BooleanPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +154,31 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Boolean entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class BooleanEntry final : public BooleanSubscriber,
+                                  public BooleanPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = BooleanSubscriber;
+  using PublisherType = BooleanPublisher;
+  using TopicType = BooleanTopic;
+  using ValueType = bool;
+  using ParamType = bool;
 
-  {{ TypeName }}Entry() = default;
+  using TimestampedValueType = TimestampedBoolean;
+
+  BooleanEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * BooleanTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  BooleanEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +208,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables Boolean topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class BooleanTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = BooleanSubscriber;
+  using PublisherType = BooleanPublisher;
+  using EntryType = BooleanEntry;
+  using ValueType = bool;
+  using ParamType = bool;
+  using TimestampedValueType = TimestampedBoolean;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "boolean";
 
-  {{ TypeName }}Topic() = default;
+  BooleanTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetBooleanTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit BooleanTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit BooleanTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +248,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +255,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +277,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +290,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +333,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +365,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/BooleanTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/BooleanTopic.inc b/ntcore/src/generated/main/native/include/networktables/BooleanTopic.inc
new file mode 100644
index 0000000..2a4d7e6
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/BooleanTopic.inc
@@ -0,0 +1,121 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/BooleanTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline BooleanSubscriber::BooleanSubscriber(
+    NT_Subscriber handle, bool defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue} {}
+
+inline bool BooleanSubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline bool BooleanSubscriber::Get(
+    bool defaultValue) const {
+  return ::nt::GetBoolean(m_subHandle, defaultValue);
+}
+
+inline TimestampedBoolean BooleanSubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedBoolean BooleanSubscriber::GetAtomic(
+    bool defaultValue) const {
+  return ::nt::GetAtomicBoolean(m_subHandle, defaultValue);
+}
+
+inline std::vector<TimestampedBoolean>
+BooleanSubscriber::ReadQueue() {
+  return ::nt::ReadQueueBoolean(m_subHandle);
+}
+
+inline BooleanTopic BooleanSubscriber::GetTopic() const {
+  return BooleanTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline BooleanPublisher::BooleanPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void BooleanPublisher::Set(bool value,
+                                         int64_t time) {
+  ::nt::SetBoolean(m_pubHandle, value, time);
+}
+
+inline void BooleanPublisher::SetDefault(bool value) {
+  ::nt::SetDefaultBoolean(m_pubHandle, value);
+}
+
+inline BooleanTopic BooleanPublisher::GetTopic() const {
+  return BooleanTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline BooleanEntry::BooleanEntry(
+    NT_Entry handle, bool defaultValue)
+    : BooleanSubscriber{handle, defaultValue},
+      BooleanPublisher{handle} {}
+
+inline BooleanTopic BooleanEntry::GetTopic() const {
+  return BooleanTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void BooleanEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline BooleanSubscriber BooleanTopic::Subscribe(
+    bool defaultValue,
+    const PubSubOptions& options) {
+  return BooleanSubscriber{
+      ::nt::Subscribe(m_handle, NT_BOOLEAN, "boolean", options),
+      defaultValue};
+}
+inline BooleanSubscriber BooleanTopic::SubscribeEx(
+    std::string_view typeString, bool defaultValue,
+    const PubSubOptions& options) {
+  return BooleanSubscriber{
+      ::nt::Subscribe(m_handle, NT_BOOLEAN, typeString, options),
+      defaultValue};
+}
+
+inline BooleanPublisher BooleanTopic::Publish(
+    const PubSubOptions& options) {
+  return BooleanPublisher{
+      ::nt::Publish(m_handle, NT_BOOLEAN, "boolean", options)};
+}
+
+inline BooleanPublisher BooleanTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return BooleanPublisher{
+      ::nt::PublishEx(m_handle, NT_BOOLEAN, typeString, properties, options)};
+}
+
+inline BooleanEntry BooleanTopic::GetEntry(
+    bool defaultValue,
+    const PubSubOptions& options) {
+  return BooleanEntry{
+      ::nt::GetEntry(m_handle, NT_BOOLEAN, "boolean", options),
+      defaultValue};
+}
+inline BooleanEntry BooleanTopic::GetEntryEx(
+    std::string_view typeString, bool defaultValue,
+    const PubSubOptions& options) {
+  return BooleanEntry{
+      ::nt::GetEntry(m_handle, NT_BOOLEAN, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/DoubleArrayTopic.h
similarity index 74%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/DoubleArrayTopic.h
index ec2a915..6f64d36 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/DoubleArrayTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+#include <utility>
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,33 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class DoubleArrayTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables DoubleArray subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class DoubleArraySubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = DoubleArrayTopic;
+  using ValueType = std::vector<double>;
+  using ParamType = std::span<const double>;
+  using TimestampedValueType = TimestampedDoubleArray;
 
-  {{ TypeName }}Subscriber() = default;
+  using SmallRetType = std::span<double>;
+  using SmallElemType = double;
+  using TimestampedValueViewType = TimestampedDoubleArrayView;
+
+
+  DoubleArraySubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * DoubleArrayTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  DoubleArraySubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,7 +68,7 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value.
    * If no value has been published, returns the stored default value.
@@ -85,7 +87,7 @@
    * @return value
    */
   SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -104,7 +106,7 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value along with its timestamp.
    * If no value has been published, returns the stored default value and a
@@ -128,7 +130,7 @@
   TimestampedValueViewType GetAtomic(
       wpi::SmallVectorImpl<SmallElemType>& buf,
       ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
@@ -153,28 +155,28 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables DoubleArray publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class DoubleArrayPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = DoubleArrayTopic;
+  using ValueType = std::vector<double>;
+  using ParamType = std::span<const double>;
 
-  {{ TypeName }}Publisher() = default;
+  using SmallRetType = std::span<double>;
+  using SmallElemType = double;
+
+  using TimestampedValueType = TimestampedDoubleArray;
+
+  DoubleArrayPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * DoubleArrayTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit DoubleArrayPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +204,34 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables DoubleArray entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class DoubleArrayEntry final : public DoubleArraySubscriber,
+                                  public DoubleArrayPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = DoubleArraySubscriber;
+  using PublisherType = DoubleArrayPublisher;
+  using TopicType = DoubleArrayTopic;
+  using ValueType = std::vector<double>;
+  using ParamType = std::span<const double>;
 
-  {{ TypeName }}Entry() = default;
+  using SmallRetType = std::span<double>;
+  using SmallElemType = double;
+
+  using TimestampedValueType = TimestampedDoubleArray;
+
+  DoubleArrayEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * DoubleArrayTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  DoubleArrayEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +261,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables DoubleArray topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class DoubleArrayTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = DoubleArraySubscriber;
+  using PublisherType = DoubleArrayPublisher;
+  using EntryType = DoubleArrayEntry;
+  using ValueType = std::vector<double>;
+  using ParamType = std::span<const double>;
+  using TimestampedValueType = TimestampedDoubleArray;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "double[]";
 
-  {{ TypeName }}Topic() = default;
+  DoubleArrayTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetDoubleArrayTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit DoubleArrayTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit DoubleArrayTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +301,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +308,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +330,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +343,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +386,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +418,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/DoubleArrayTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/DoubleArrayTopic.inc b/ntcore/src/generated/main/native/include/networktables/DoubleArrayTopic.inc
new file mode 100644
index 0000000..bab8dd0
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/DoubleArrayTopic.inc
@@ -0,0 +1,137 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/DoubleArrayTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline DoubleArraySubscriber::DoubleArraySubscriber(
+    NT_Subscriber handle, std::span<const double> defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue.begin(), defaultValue.end()} {}
+
+inline std::vector<double> DoubleArraySubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline std::vector<double> DoubleArraySubscriber::Get(
+    std::span<const double> defaultValue) const {
+  return ::nt::GetDoubleArray(m_subHandle, defaultValue);
+}
+
+inline std::span<double> DoubleArraySubscriber::Get(wpi::SmallVectorImpl<double>& buf) const {
+  return Get(buf, m_defaultValue);
+}
+
+inline std::span<double> DoubleArraySubscriber::Get(wpi::SmallVectorImpl<double>& buf, std::span<const double> defaultValue) const {
+  return nt::GetDoubleArray(m_subHandle, buf, defaultValue);
+}
+
+inline TimestampedDoubleArray DoubleArraySubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedDoubleArray DoubleArraySubscriber::GetAtomic(
+    std::span<const double> defaultValue) const {
+  return ::nt::GetAtomicDoubleArray(m_subHandle, defaultValue);
+}
+
+inline TimestampedDoubleArrayView DoubleArraySubscriber::GetAtomic(wpi::SmallVectorImpl<double>& buf) const {
+  return GetAtomic(buf, m_defaultValue);
+}
+
+inline TimestampedDoubleArrayView DoubleArraySubscriber::GetAtomic(wpi::SmallVectorImpl<double>& buf, std::span<const double> defaultValue) const {
+  return nt::GetAtomicDoubleArray(m_subHandle, buf, defaultValue);
+}
+
+inline std::vector<TimestampedDoubleArray>
+DoubleArraySubscriber::ReadQueue() {
+  return ::nt::ReadQueueDoubleArray(m_subHandle);
+}
+
+inline DoubleArrayTopic DoubleArraySubscriber::GetTopic() const {
+  return DoubleArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline DoubleArrayPublisher::DoubleArrayPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void DoubleArrayPublisher::Set(std::span<const double> value,
+                                         int64_t time) {
+  ::nt::SetDoubleArray(m_pubHandle, value, time);
+}
+
+inline void DoubleArrayPublisher::SetDefault(std::span<const double> value) {
+  ::nt::SetDefaultDoubleArray(m_pubHandle, value);
+}
+
+inline DoubleArrayTopic DoubleArrayPublisher::GetTopic() const {
+  return DoubleArrayTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline DoubleArrayEntry::DoubleArrayEntry(
+    NT_Entry handle, std::span<const double> defaultValue)
+    : DoubleArraySubscriber{handle, defaultValue},
+      DoubleArrayPublisher{handle} {}
+
+inline DoubleArrayTopic DoubleArrayEntry::GetTopic() const {
+  return DoubleArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void DoubleArrayEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline DoubleArraySubscriber DoubleArrayTopic::Subscribe(
+    std::span<const double> defaultValue,
+    const PubSubOptions& options) {
+  return DoubleArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_DOUBLE_ARRAY, "double[]", options),
+      defaultValue};
+}
+inline DoubleArraySubscriber DoubleArrayTopic::SubscribeEx(
+    std::string_view typeString, std::span<const double> defaultValue,
+    const PubSubOptions& options) {
+  return DoubleArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_DOUBLE_ARRAY, typeString, options),
+      defaultValue};
+}
+
+inline DoubleArrayPublisher DoubleArrayTopic::Publish(
+    const PubSubOptions& options) {
+  return DoubleArrayPublisher{
+      ::nt::Publish(m_handle, NT_DOUBLE_ARRAY, "double[]", options)};
+}
+
+inline DoubleArrayPublisher DoubleArrayTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return DoubleArrayPublisher{
+      ::nt::PublishEx(m_handle, NT_DOUBLE_ARRAY, typeString, properties, options)};
+}
+
+inline DoubleArrayEntry DoubleArrayTopic::GetEntry(
+    std::span<const double> defaultValue,
+    const PubSubOptions& options) {
+  return DoubleArrayEntry{
+      ::nt::GetEntry(m_handle, NT_DOUBLE_ARRAY, "double[]", options),
+      defaultValue};
+}
+inline DoubleArrayEntry DoubleArrayTopic::GetEntryEx(
+    std::string_view typeString, std::span<const double> defaultValue,
+    const PubSubOptions& options) {
+  return DoubleArrayEntry{
+      ::nt::GetEntry(m_handle, NT_DOUBLE_ARRAY, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/DoubleTopic.h
similarity index 64%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/DoubleTopic.h
index ec2a915..bcb1751 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/DoubleTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,29 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class DoubleTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables Double subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class DoubleSubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = DoubleTopic;
+  using ValueType = double;
+  using ParamType = double;
+  using TimestampedValueType = TimestampedDouble;
 
-  {{ TypeName }}Subscriber() = default;
+
+  DoubleSubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * DoubleTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  DoubleSubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,27 +64,8 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value.
-   * If no value has been published, returns the stored default value.
-   *
-   * @param buf storage for returned value
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value.
-   * If no value has been published, returns the passed defaultValue.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
    * timestamp of 0.
@@ -104,32 +83,8 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the stored default value and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the passed defaultValue and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf,
-      ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
    *
@@ -153,28 +108,25 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables Double publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class DoublePublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = DoubleTopic;
+  using ValueType = double;
+  using ParamType = double;
 
-  {{ TypeName }}Publisher() = default;
+  using TimestampedValueType = TimestampedDouble;
+
+  DoublePublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * DoubleTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit DoublePublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +154,31 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Double entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class DoubleEntry final : public DoubleSubscriber,
+                                  public DoublePublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = DoubleSubscriber;
+  using PublisherType = DoublePublisher;
+  using TopicType = DoubleTopic;
+  using ValueType = double;
+  using ParamType = double;
 
-  {{ TypeName }}Entry() = default;
+  using TimestampedValueType = TimestampedDouble;
+
+  DoubleEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * DoubleTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  DoubleEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +208,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables Double topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class DoubleTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = DoubleSubscriber;
+  using PublisherType = DoublePublisher;
+  using EntryType = DoubleEntry;
+  using ValueType = double;
+  using ParamType = double;
+  using TimestampedValueType = TimestampedDouble;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "double";
 
-  {{ TypeName }}Topic() = default;
+  DoubleTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetDoubleTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit DoubleTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit DoubleTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +248,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +255,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +277,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +290,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +333,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +365,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/DoubleTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/DoubleTopic.inc b/ntcore/src/generated/main/native/include/networktables/DoubleTopic.inc
new file mode 100644
index 0000000..49b1c4c
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/DoubleTopic.inc
@@ -0,0 +1,121 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/DoubleTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline DoubleSubscriber::DoubleSubscriber(
+    NT_Subscriber handle, double defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue} {}
+
+inline double DoubleSubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline double DoubleSubscriber::Get(
+    double defaultValue) const {
+  return ::nt::GetDouble(m_subHandle, defaultValue);
+}
+
+inline TimestampedDouble DoubleSubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedDouble DoubleSubscriber::GetAtomic(
+    double defaultValue) const {
+  return ::nt::GetAtomicDouble(m_subHandle, defaultValue);
+}
+
+inline std::vector<TimestampedDouble>
+DoubleSubscriber::ReadQueue() {
+  return ::nt::ReadQueueDouble(m_subHandle);
+}
+
+inline DoubleTopic DoubleSubscriber::GetTopic() const {
+  return DoubleTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline DoublePublisher::DoublePublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void DoublePublisher::Set(double value,
+                                         int64_t time) {
+  ::nt::SetDouble(m_pubHandle, value, time);
+}
+
+inline void DoublePublisher::SetDefault(double value) {
+  ::nt::SetDefaultDouble(m_pubHandle, value);
+}
+
+inline DoubleTopic DoublePublisher::GetTopic() const {
+  return DoubleTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline DoubleEntry::DoubleEntry(
+    NT_Entry handle, double defaultValue)
+    : DoubleSubscriber{handle, defaultValue},
+      DoublePublisher{handle} {}
+
+inline DoubleTopic DoubleEntry::GetTopic() const {
+  return DoubleTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void DoubleEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline DoubleSubscriber DoubleTopic::Subscribe(
+    double defaultValue,
+    const PubSubOptions& options) {
+  return DoubleSubscriber{
+      ::nt::Subscribe(m_handle, NT_DOUBLE, "double", options),
+      defaultValue};
+}
+inline DoubleSubscriber DoubleTopic::SubscribeEx(
+    std::string_view typeString, double defaultValue,
+    const PubSubOptions& options) {
+  return DoubleSubscriber{
+      ::nt::Subscribe(m_handle, NT_DOUBLE, typeString, options),
+      defaultValue};
+}
+
+inline DoublePublisher DoubleTopic::Publish(
+    const PubSubOptions& options) {
+  return DoublePublisher{
+      ::nt::Publish(m_handle, NT_DOUBLE, "double", options)};
+}
+
+inline DoublePublisher DoubleTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return DoublePublisher{
+      ::nt::PublishEx(m_handle, NT_DOUBLE, typeString, properties, options)};
+}
+
+inline DoubleEntry DoubleTopic::GetEntry(
+    double defaultValue,
+    const PubSubOptions& options) {
+  return DoubleEntry{
+      ::nt::GetEntry(m_handle, NT_DOUBLE, "double", options),
+      defaultValue};
+}
+inline DoubleEntry DoubleTopic::GetEntryEx(
+    std::string_view typeString, double defaultValue,
+    const PubSubOptions& options) {
+  return DoubleEntry{
+      ::nt::GetEntry(m_handle, NT_DOUBLE, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/FloatArrayTopic.h
similarity index 74%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/FloatArrayTopic.h
index ec2a915..a0d2b66 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/FloatArrayTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+#include <utility>
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,33 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class FloatArrayTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables FloatArray subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class FloatArraySubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = FloatArrayTopic;
+  using ValueType = std::vector<float>;
+  using ParamType = std::span<const float>;
+  using TimestampedValueType = TimestampedFloatArray;
 
-  {{ TypeName }}Subscriber() = default;
+  using SmallRetType = std::span<float>;
+  using SmallElemType = float;
+  using TimestampedValueViewType = TimestampedFloatArrayView;
+
+
+  FloatArraySubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * FloatArrayTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  FloatArraySubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,7 +68,7 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value.
    * If no value has been published, returns the stored default value.
@@ -85,7 +87,7 @@
    * @return value
    */
   SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -104,7 +106,7 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value along with its timestamp.
    * If no value has been published, returns the stored default value and a
@@ -128,7 +130,7 @@
   TimestampedValueViewType GetAtomic(
       wpi::SmallVectorImpl<SmallElemType>& buf,
       ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
@@ -153,28 +155,28 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables FloatArray publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class FloatArrayPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = FloatArrayTopic;
+  using ValueType = std::vector<float>;
+  using ParamType = std::span<const float>;
 
-  {{ TypeName }}Publisher() = default;
+  using SmallRetType = std::span<float>;
+  using SmallElemType = float;
+
+  using TimestampedValueType = TimestampedFloatArray;
+
+  FloatArrayPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * FloatArrayTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit FloatArrayPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +204,34 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables FloatArray entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class FloatArrayEntry final : public FloatArraySubscriber,
+                                  public FloatArrayPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = FloatArraySubscriber;
+  using PublisherType = FloatArrayPublisher;
+  using TopicType = FloatArrayTopic;
+  using ValueType = std::vector<float>;
+  using ParamType = std::span<const float>;
 
-  {{ TypeName }}Entry() = default;
+  using SmallRetType = std::span<float>;
+  using SmallElemType = float;
+
+  using TimestampedValueType = TimestampedFloatArray;
+
+  FloatArrayEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * FloatArrayTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  FloatArrayEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +261,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables FloatArray topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class FloatArrayTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = FloatArraySubscriber;
+  using PublisherType = FloatArrayPublisher;
+  using EntryType = FloatArrayEntry;
+  using ValueType = std::vector<float>;
+  using ParamType = std::span<const float>;
+  using TimestampedValueType = TimestampedFloatArray;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "float[]";
 
-  {{ TypeName }}Topic() = default;
+  FloatArrayTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetFloatArrayTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit FloatArrayTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit FloatArrayTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +301,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +308,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +330,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +343,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +386,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +418,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/FloatArrayTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/FloatArrayTopic.inc b/ntcore/src/generated/main/native/include/networktables/FloatArrayTopic.inc
new file mode 100644
index 0000000..9357076
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/FloatArrayTopic.inc
@@ -0,0 +1,137 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/FloatArrayTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline FloatArraySubscriber::FloatArraySubscriber(
+    NT_Subscriber handle, std::span<const float> defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue.begin(), defaultValue.end()} {}
+
+inline std::vector<float> FloatArraySubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline std::vector<float> FloatArraySubscriber::Get(
+    std::span<const float> defaultValue) const {
+  return ::nt::GetFloatArray(m_subHandle, defaultValue);
+}
+
+inline std::span<float> FloatArraySubscriber::Get(wpi::SmallVectorImpl<float>& buf) const {
+  return Get(buf, m_defaultValue);
+}
+
+inline std::span<float> FloatArraySubscriber::Get(wpi::SmallVectorImpl<float>& buf, std::span<const float> defaultValue) const {
+  return nt::GetFloatArray(m_subHandle, buf, defaultValue);
+}
+
+inline TimestampedFloatArray FloatArraySubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedFloatArray FloatArraySubscriber::GetAtomic(
+    std::span<const float> defaultValue) const {
+  return ::nt::GetAtomicFloatArray(m_subHandle, defaultValue);
+}
+
+inline TimestampedFloatArrayView FloatArraySubscriber::GetAtomic(wpi::SmallVectorImpl<float>& buf) const {
+  return GetAtomic(buf, m_defaultValue);
+}
+
+inline TimestampedFloatArrayView FloatArraySubscriber::GetAtomic(wpi::SmallVectorImpl<float>& buf, std::span<const float> defaultValue) const {
+  return nt::GetAtomicFloatArray(m_subHandle, buf, defaultValue);
+}
+
+inline std::vector<TimestampedFloatArray>
+FloatArraySubscriber::ReadQueue() {
+  return ::nt::ReadQueueFloatArray(m_subHandle);
+}
+
+inline FloatArrayTopic FloatArraySubscriber::GetTopic() const {
+  return FloatArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline FloatArrayPublisher::FloatArrayPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void FloatArrayPublisher::Set(std::span<const float> value,
+                                         int64_t time) {
+  ::nt::SetFloatArray(m_pubHandle, value, time);
+}
+
+inline void FloatArrayPublisher::SetDefault(std::span<const float> value) {
+  ::nt::SetDefaultFloatArray(m_pubHandle, value);
+}
+
+inline FloatArrayTopic FloatArrayPublisher::GetTopic() const {
+  return FloatArrayTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline FloatArrayEntry::FloatArrayEntry(
+    NT_Entry handle, std::span<const float> defaultValue)
+    : FloatArraySubscriber{handle, defaultValue},
+      FloatArrayPublisher{handle} {}
+
+inline FloatArrayTopic FloatArrayEntry::GetTopic() const {
+  return FloatArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void FloatArrayEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline FloatArraySubscriber FloatArrayTopic::Subscribe(
+    std::span<const float> defaultValue,
+    const PubSubOptions& options) {
+  return FloatArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_FLOAT_ARRAY, "float[]", options),
+      defaultValue};
+}
+inline FloatArraySubscriber FloatArrayTopic::SubscribeEx(
+    std::string_view typeString, std::span<const float> defaultValue,
+    const PubSubOptions& options) {
+  return FloatArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_FLOAT_ARRAY, typeString, options),
+      defaultValue};
+}
+
+inline FloatArrayPublisher FloatArrayTopic::Publish(
+    const PubSubOptions& options) {
+  return FloatArrayPublisher{
+      ::nt::Publish(m_handle, NT_FLOAT_ARRAY, "float[]", options)};
+}
+
+inline FloatArrayPublisher FloatArrayTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return FloatArrayPublisher{
+      ::nt::PublishEx(m_handle, NT_FLOAT_ARRAY, typeString, properties, options)};
+}
+
+inline FloatArrayEntry FloatArrayTopic::GetEntry(
+    std::span<const float> defaultValue,
+    const PubSubOptions& options) {
+  return FloatArrayEntry{
+      ::nt::GetEntry(m_handle, NT_FLOAT_ARRAY, "float[]", options),
+      defaultValue};
+}
+inline FloatArrayEntry FloatArrayTopic::GetEntryEx(
+    std::string_view typeString, std::span<const float> defaultValue,
+    const PubSubOptions& options) {
+  return FloatArrayEntry{
+      ::nt::GetEntry(m_handle, NT_FLOAT_ARRAY, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/FloatTopic.h
similarity index 64%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/FloatTopic.h
index ec2a915..9a83834 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/FloatTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,29 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class FloatTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables Float subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class FloatSubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = FloatTopic;
+  using ValueType = float;
+  using ParamType = float;
+  using TimestampedValueType = TimestampedFloat;
 
-  {{ TypeName }}Subscriber() = default;
+
+  FloatSubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * FloatTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  FloatSubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,27 +64,8 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value.
-   * If no value has been published, returns the stored default value.
-   *
-   * @param buf storage for returned value
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value.
-   * If no value has been published, returns the passed defaultValue.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
    * timestamp of 0.
@@ -104,32 +83,8 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the stored default value and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the passed defaultValue and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf,
-      ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
    *
@@ -153,28 +108,25 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables Float publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class FloatPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = FloatTopic;
+  using ValueType = float;
+  using ParamType = float;
 
-  {{ TypeName }}Publisher() = default;
+  using TimestampedValueType = TimestampedFloat;
+
+  FloatPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * FloatTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit FloatPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +154,31 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Float entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class FloatEntry final : public FloatSubscriber,
+                                  public FloatPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = FloatSubscriber;
+  using PublisherType = FloatPublisher;
+  using TopicType = FloatTopic;
+  using ValueType = float;
+  using ParamType = float;
 
-  {{ TypeName }}Entry() = default;
+  using TimestampedValueType = TimestampedFloat;
+
+  FloatEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * FloatTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  FloatEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +208,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables Float topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class FloatTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = FloatSubscriber;
+  using PublisherType = FloatPublisher;
+  using EntryType = FloatEntry;
+  using ValueType = float;
+  using ParamType = float;
+  using TimestampedValueType = TimestampedFloat;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "float";
 
-  {{ TypeName }}Topic() = default;
+  FloatTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetFloatTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit FloatTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit FloatTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +248,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +255,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +277,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +290,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +333,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +365,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/FloatTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/FloatTopic.inc b/ntcore/src/generated/main/native/include/networktables/FloatTopic.inc
new file mode 100644
index 0000000..9df0f79
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/FloatTopic.inc
@@ -0,0 +1,121 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/FloatTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline FloatSubscriber::FloatSubscriber(
+    NT_Subscriber handle, float defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue} {}
+
+inline float FloatSubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline float FloatSubscriber::Get(
+    float defaultValue) const {
+  return ::nt::GetFloat(m_subHandle, defaultValue);
+}
+
+inline TimestampedFloat FloatSubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedFloat FloatSubscriber::GetAtomic(
+    float defaultValue) const {
+  return ::nt::GetAtomicFloat(m_subHandle, defaultValue);
+}
+
+inline std::vector<TimestampedFloat>
+FloatSubscriber::ReadQueue() {
+  return ::nt::ReadQueueFloat(m_subHandle);
+}
+
+inline FloatTopic FloatSubscriber::GetTopic() const {
+  return FloatTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline FloatPublisher::FloatPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void FloatPublisher::Set(float value,
+                                         int64_t time) {
+  ::nt::SetFloat(m_pubHandle, value, time);
+}
+
+inline void FloatPublisher::SetDefault(float value) {
+  ::nt::SetDefaultFloat(m_pubHandle, value);
+}
+
+inline FloatTopic FloatPublisher::GetTopic() const {
+  return FloatTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline FloatEntry::FloatEntry(
+    NT_Entry handle, float defaultValue)
+    : FloatSubscriber{handle, defaultValue},
+      FloatPublisher{handle} {}
+
+inline FloatTopic FloatEntry::GetTopic() const {
+  return FloatTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void FloatEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline FloatSubscriber FloatTopic::Subscribe(
+    float defaultValue,
+    const PubSubOptions& options) {
+  return FloatSubscriber{
+      ::nt::Subscribe(m_handle, NT_FLOAT, "float", options),
+      defaultValue};
+}
+inline FloatSubscriber FloatTopic::SubscribeEx(
+    std::string_view typeString, float defaultValue,
+    const PubSubOptions& options) {
+  return FloatSubscriber{
+      ::nt::Subscribe(m_handle, NT_FLOAT, typeString, options),
+      defaultValue};
+}
+
+inline FloatPublisher FloatTopic::Publish(
+    const PubSubOptions& options) {
+  return FloatPublisher{
+      ::nt::Publish(m_handle, NT_FLOAT, "float", options)};
+}
+
+inline FloatPublisher FloatTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return FloatPublisher{
+      ::nt::PublishEx(m_handle, NT_FLOAT, typeString, properties, options)};
+}
+
+inline FloatEntry FloatTopic::GetEntry(
+    float defaultValue,
+    const PubSubOptions& options) {
+  return FloatEntry{
+      ::nt::GetEntry(m_handle, NT_FLOAT, "float", options),
+      defaultValue};
+}
+inline FloatEntry FloatTopic::GetEntryEx(
+    std::string_view typeString, float defaultValue,
+    const PubSubOptions& options) {
+  return FloatEntry{
+      ::nt::GetEntry(m_handle, NT_FLOAT, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/IntegerArrayTopic.h
similarity index 74%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/IntegerArrayTopic.h
index ec2a915..a99823a 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/IntegerArrayTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+#include <utility>
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,33 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class IntegerArrayTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables IntegerArray subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class IntegerArraySubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = IntegerArrayTopic;
+  using ValueType = std::vector<int64_t>;
+  using ParamType = std::span<const int64_t>;
+  using TimestampedValueType = TimestampedIntegerArray;
 
-  {{ TypeName }}Subscriber() = default;
+  using SmallRetType = std::span<int64_t>;
+  using SmallElemType = int64_t;
+  using TimestampedValueViewType = TimestampedIntegerArrayView;
+
+
+  IntegerArraySubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * IntegerArrayTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  IntegerArraySubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,7 +68,7 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value.
    * If no value has been published, returns the stored default value.
@@ -85,7 +87,7 @@
    * @return value
    */
   SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -104,7 +106,7 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value along with its timestamp.
    * If no value has been published, returns the stored default value and a
@@ -128,7 +130,7 @@
   TimestampedValueViewType GetAtomic(
       wpi::SmallVectorImpl<SmallElemType>& buf,
       ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
@@ -153,28 +155,28 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables IntegerArray publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class IntegerArrayPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = IntegerArrayTopic;
+  using ValueType = std::vector<int64_t>;
+  using ParamType = std::span<const int64_t>;
 
-  {{ TypeName }}Publisher() = default;
+  using SmallRetType = std::span<int64_t>;
+  using SmallElemType = int64_t;
+
+  using TimestampedValueType = TimestampedIntegerArray;
+
+  IntegerArrayPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * IntegerArrayTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit IntegerArrayPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +204,34 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables IntegerArray entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class IntegerArrayEntry final : public IntegerArraySubscriber,
+                                  public IntegerArrayPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = IntegerArraySubscriber;
+  using PublisherType = IntegerArrayPublisher;
+  using TopicType = IntegerArrayTopic;
+  using ValueType = std::vector<int64_t>;
+  using ParamType = std::span<const int64_t>;
 
-  {{ TypeName }}Entry() = default;
+  using SmallRetType = std::span<int64_t>;
+  using SmallElemType = int64_t;
+
+  using TimestampedValueType = TimestampedIntegerArray;
+
+  IntegerArrayEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * IntegerArrayTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  IntegerArrayEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +261,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables IntegerArray topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class IntegerArrayTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = IntegerArraySubscriber;
+  using PublisherType = IntegerArrayPublisher;
+  using EntryType = IntegerArrayEntry;
+  using ValueType = std::vector<int64_t>;
+  using ParamType = std::span<const int64_t>;
+  using TimestampedValueType = TimestampedIntegerArray;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "int[]";
 
-  {{ TypeName }}Topic() = default;
+  IntegerArrayTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetIntegerArrayTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit IntegerArrayTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit IntegerArrayTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +301,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +308,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +330,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +343,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +386,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +418,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/IntegerArrayTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/IntegerArrayTopic.inc b/ntcore/src/generated/main/native/include/networktables/IntegerArrayTopic.inc
new file mode 100644
index 0000000..ba3d00d
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/IntegerArrayTopic.inc
@@ -0,0 +1,137 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/IntegerArrayTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline IntegerArraySubscriber::IntegerArraySubscriber(
+    NT_Subscriber handle, std::span<const int64_t> defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue.begin(), defaultValue.end()} {}
+
+inline std::vector<int64_t> IntegerArraySubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline std::vector<int64_t> IntegerArraySubscriber::Get(
+    std::span<const int64_t> defaultValue) const {
+  return ::nt::GetIntegerArray(m_subHandle, defaultValue);
+}
+
+inline std::span<int64_t> IntegerArraySubscriber::Get(wpi::SmallVectorImpl<int64_t>& buf) const {
+  return Get(buf, m_defaultValue);
+}
+
+inline std::span<int64_t> IntegerArraySubscriber::Get(wpi::SmallVectorImpl<int64_t>& buf, std::span<const int64_t> defaultValue) const {
+  return nt::GetIntegerArray(m_subHandle, buf, defaultValue);
+}
+
+inline TimestampedIntegerArray IntegerArraySubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedIntegerArray IntegerArraySubscriber::GetAtomic(
+    std::span<const int64_t> defaultValue) const {
+  return ::nt::GetAtomicIntegerArray(m_subHandle, defaultValue);
+}
+
+inline TimestampedIntegerArrayView IntegerArraySubscriber::GetAtomic(wpi::SmallVectorImpl<int64_t>& buf) const {
+  return GetAtomic(buf, m_defaultValue);
+}
+
+inline TimestampedIntegerArrayView IntegerArraySubscriber::GetAtomic(wpi::SmallVectorImpl<int64_t>& buf, std::span<const int64_t> defaultValue) const {
+  return nt::GetAtomicIntegerArray(m_subHandle, buf, defaultValue);
+}
+
+inline std::vector<TimestampedIntegerArray>
+IntegerArraySubscriber::ReadQueue() {
+  return ::nt::ReadQueueIntegerArray(m_subHandle);
+}
+
+inline IntegerArrayTopic IntegerArraySubscriber::GetTopic() const {
+  return IntegerArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline IntegerArrayPublisher::IntegerArrayPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void IntegerArrayPublisher::Set(std::span<const int64_t> value,
+                                         int64_t time) {
+  ::nt::SetIntegerArray(m_pubHandle, value, time);
+}
+
+inline void IntegerArrayPublisher::SetDefault(std::span<const int64_t> value) {
+  ::nt::SetDefaultIntegerArray(m_pubHandle, value);
+}
+
+inline IntegerArrayTopic IntegerArrayPublisher::GetTopic() const {
+  return IntegerArrayTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline IntegerArrayEntry::IntegerArrayEntry(
+    NT_Entry handle, std::span<const int64_t> defaultValue)
+    : IntegerArraySubscriber{handle, defaultValue},
+      IntegerArrayPublisher{handle} {}
+
+inline IntegerArrayTopic IntegerArrayEntry::GetTopic() const {
+  return IntegerArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void IntegerArrayEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline IntegerArraySubscriber IntegerArrayTopic::Subscribe(
+    std::span<const int64_t> defaultValue,
+    const PubSubOptions& options) {
+  return IntegerArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_INTEGER_ARRAY, "int[]", options),
+      defaultValue};
+}
+inline IntegerArraySubscriber IntegerArrayTopic::SubscribeEx(
+    std::string_view typeString, std::span<const int64_t> defaultValue,
+    const PubSubOptions& options) {
+  return IntegerArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_INTEGER_ARRAY, typeString, options),
+      defaultValue};
+}
+
+inline IntegerArrayPublisher IntegerArrayTopic::Publish(
+    const PubSubOptions& options) {
+  return IntegerArrayPublisher{
+      ::nt::Publish(m_handle, NT_INTEGER_ARRAY, "int[]", options)};
+}
+
+inline IntegerArrayPublisher IntegerArrayTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return IntegerArrayPublisher{
+      ::nt::PublishEx(m_handle, NT_INTEGER_ARRAY, typeString, properties, options)};
+}
+
+inline IntegerArrayEntry IntegerArrayTopic::GetEntry(
+    std::span<const int64_t> defaultValue,
+    const PubSubOptions& options) {
+  return IntegerArrayEntry{
+      ::nt::GetEntry(m_handle, NT_INTEGER_ARRAY, "int[]", options),
+      defaultValue};
+}
+inline IntegerArrayEntry IntegerArrayTopic::GetEntryEx(
+    std::string_view typeString, std::span<const int64_t> defaultValue,
+    const PubSubOptions& options) {
+  return IntegerArrayEntry{
+      ::nt::GetEntry(m_handle, NT_INTEGER_ARRAY, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/IntegerTopic.h
similarity index 64%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/IntegerTopic.h
index ec2a915..0d5ab21 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/IntegerTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,29 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class IntegerTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables Integer subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class IntegerSubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = IntegerTopic;
+  using ValueType = int64_t;
+  using ParamType = int64_t;
+  using TimestampedValueType = TimestampedInteger;
 
-  {{ TypeName }}Subscriber() = default;
+
+  IntegerSubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * IntegerTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  IntegerSubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,27 +64,8 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value.
-   * If no value has been published, returns the stored default value.
-   *
-   * @param buf storage for returned value
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value.
-   * If no value has been published, returns the passed defaultValue.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
    * timestamp of 0.
@@ -104,32 +83,8 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the stored default value and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the passed defaultValue and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf,
-      ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
    *
@@ -153,28 +108,25 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables Integer publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class IntegerPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = IntegerTopic;
+  using ValueType = int64_t;
+  using ParamType = int64_t;
 
-  {{ TypeName }}Publisher() = default;
+  using TimestampedValueType = TimestampedInteger;
+
+  IntegerPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * IntegerTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit IntegerPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +154,31 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Integer entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class IntegerEntry final : public IntegerSubscriber,
+                                  public IntegerPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = IntegerSubscriber;
+  using PublisherType = IntegerPublisher;
+  using TopicType = IntegerTopic;
+  using ValueType = int64_t;
+  using ParamType = int64_t;
 
-  {{ TypeName }}Entry() = default;
+  using TimestampedValueType = TimestampedInteger;
+
+  IntegerEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * IntegerTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  IntegerEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +208,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables Integer topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class IntegerTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = IntegerSubscriber;
+  using PublisherType = IntegerPublisher;
+  using EntryType = IntegerEntry;
+  using ValueType = int64_t;
+  using ParamType = int64_t;
+  using TimestampedValueType = TimestampedInteger;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "int";
 
-  {{ TypeName }}Topic() = default;
+  IntegerTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetIntegerTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit IntegerTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit IntegerTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +248,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +255,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +277,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +290,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +333,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +365,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/IntegerTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/IntegerTopic.inc b/ntcore/src/generated/main/native/include/networktables/IntegerTopic.inc
new file mode 100644
index 0000000..2ca31e5
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/IntegerTopic.inc
@@ -0,0 +1,121 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/IntegerTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline IntegerSubscriber::IntegerSubscriber(
+    NT_Subscriber handle, int64_t defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue} {}
+
+inline int64_t IntegerSubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline int64_t IntegerSubscriber::Get(
+    int64_t defaultValue) const {
+  return ::nt::GetInteger(m_subHandle, defaultValue);
+}
+
+inline TimestampedInteger IntegerSubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedInteger IntegerSubscriber::GetAtomic(
+    int64_t defaultValue) const {
+  return ::nt::GetAtomicInteger(m_subHandle, defaultValue);
+}
+
+inline std::vector<TimestampedInteger>
+IntegerSubscriber::ReadQueue() {
+  return ::nt::ReadQueueInteger(m_subHandle);
+}
+
+inline IntegerTopic IntegerSubscriber::GetTopic() const {
+  return IntegerTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline IntegerPublisher::IntegerPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void IntegerPublisher::Set(int64_t value,
+                                         int64_t time) {
+  ::nt::SetInteger(m_pubHandle, value, time);
+}
+
+inline void IntegerPublisher::SetDefault(int64_t value) {
+  ::nt::SetDefaultInteger(m_pubHandle, value);
+}
+
+inline IntegerTopic IntegerPublisher::GetTopic() const {
+  return IntegerTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline IntegerEntry::IntegerEntry(
+    NT_Entry handle, int64_t defaultValue)
+    : IntegerSubscriber{handle, defaultValue},
+      IntegerPublisher{handle} {}
+
+inline IntegerTopic IntegerEntry::GetTopic() const {
+  return IntegerTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void IntegerEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline IntegerSubscriber IntegerTopic::Subscribe(
+    int64_t defaultValue,
+    const PubSubOptions& options) {
+  return IntegerSubscriber{
+      ::nt::Subscribe(m_handle, NT_INTEGER, "int", options),
+      defaultValue};
+}
+inline IntegerSubscriber IntegerTopic::SubscribeEx(
+    std::string_view typeString, int64_t defaultValue,
+    const PubSubOptions& options) {
+  return IntegerSubscriber{
+      ::nt::Subscribe(m_handle, NT_INTEGER, typeString, options),
+      defaultValue};
+}
+
+inline IntegerPublisher IntegerTopic::Publish(
+    const PubSubOptions& options) {
+  return IntegerPublisher{
+      ::nt::Publish(m_handle, NT_INTEGER, "int", options)};
+}
+
+inline IntegerPublisher IntegerTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return IntegerPublisher{
+      ::nt::PublishEx(m_handle, NT_INTEGER, typeString, properties, options)};
+}
+
+inline IntegerEntry IntegerTopic::GetEntry(
+    int64_t defaultValue,
+    const PubSubOptions& options) {
+  return IntegerEntry{
+      ::nt::GetEntry(m_handle, NT_INTEGER, "int", options),
+      defaultValue};
+}
+inline IntegerEntry IntegerTopic::GetEntryEx(
+    std::string_view typeString, int64_t defaultValue,
+    const PubSubOptions& options) {
+  return IntegerEntry{
+      ::nt::GetEntry(m_handle, NT_INTEGER, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/RawTopic.h
similarity index 61%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/RawTopic.h
index ec2a915..e124fe1 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/RawTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+#include <utility>
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,33 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class RawTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables Raw subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class RawSubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = RawTopic;
+  using ValueType = std::vector<uint8_t>;
+  using ParamType = std::span<const uint8_t>;
+  using TimestampedValueType = TimestampedRaw;
 
-  {{ TypeName }}Subscriber() = default;
+  using SmallRetType = std::span<uint8_t>;
+  using SmallElemType = uint8_t;
+  using TimestampedValueViewType = TimestampedRawView;
+
+
+  RawSubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * RawTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  RawSubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,7 +68,7 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value.
    * If no value has been published, returns the stored default value.
@@ -85,7 +87,7 @@
    * @return value
    */
   SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -104,7 +106,7 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value along with its timestamp.
    * If no value has been published, returns the stored default value and a
@@ -128,7 +130,7 @@
   TimestampedValueViewType GetAtomic(
       wpi::SmallVectorImpl<SmallElemType>& buf,
       ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
@@ -153,28 +155,28 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables Raw publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class RawPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = RawTopic;
+  using ValueType = std::vector<uint8_t>;
+  using ParamType = std::span<const uint8_t>;
 
-  {{ TypeName }}Publisher() = default;
+  using SmallRetType = std::span<uint8_t>;
+  using SmallElemType = uint8_t;
+
+  using TimestampedValueType = TimestampedRaw;
+
+  RawPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * RawTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit RawPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +204,34 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables Raw entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class RawEntry final : public RawSubscriber,
+                                  public RawPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = RawSubscriber;
+  using PublisherType = RawPublisher;
+  using TopicType = RawTopic;
+  using ValueType = std::vector<uint8_t>;
+  using ParamType = std::span<const uint8_t>;
 
-  {{ TypeName }}Entry() = default;
+  using SmallRetType = std::span<uint8_t>;
+  using SmallElemType = uint8_t;
+
+  using TimestampedValueType = TimestampedRaw;
+
+  RawEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * RawTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  RawEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +261,33 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables Raw topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class RawTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
-  /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  using SubscriberType = RawSubscriber;
+  using PublisherType = RawPublisher;
+  using EntryType = RawEntry;
+  using ValueType = std::vector<uint8_t>;
+  using ParamType = std::span<const uint8_t>;
+  using TimestampedValueType = TimestampedRaw;
 
-  {{ TypeName }}Topic() = default;
+  RawTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetRawTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit RawTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit RawTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +299,8 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
    * @param typeString type string
-{% endif %}
+
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,30 +308,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
-      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
-  /**
-   * Create a new subscriber to the topic, with specific type string.
-   *
-   * <p>The subscriber is only active as long as the returned object
-   * is not destroyed.
-   *
-   * @note Subscribers that do not match the published data type do not return
-   *     any values. To determine if the data type matches, use the appropriate
-   *     Topic functions.
-   *
-   * @param typeString type string
-   * @param defaultValue default value used when a default is not provided to a
-   *        getter function
-   * @param options subscribe options
-   * @return subscriber
-   */
-  [[nodiscard]]
-  SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +322,13 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
    * @param typeString type string
-{% endif %}
+
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(std::string_view typeString, const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,45 +367,18 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
    * @param typeString type string
-{% endif %}
+
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(std::string_view typeString, ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
-  /**
-   * Create a new entry for the topic, with specific type string.
-   *
-   * Entries act as a combination of a subscriber and a weak publisher. The
-   * subscriber is active as long as the entry is not destroyed. The publisher
-   * is created when the entry is first written to, and remains active until
-   * either Unpublish() is called or the entry is destroyed.
-   *
-   * @note It is not possible to use two different data types with the same
-   *     topic. Conflicts between publishers are typically resolved by the
-   *     server on a first-come, first-served basis. Any published values that
-   *     do not match the topic's data type are dropped (ignored), and the entry
-   *     will show no new values if the data type does not match. To determine
-   *     if the data type matches, use the appropriate Topic functions.
-   *
-   * @param typeString type string
-   * @param defaultValue default value used when a default is not provided to a
-   *        getter function
-   * @param options publish and/or subscribe options
-   * @return entry
-   */
-  [[nodiscard]]
-  EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
-                       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/RawTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/RawTopic.inc b/ntcore/src/generated/main/native/include/networktables/RawTopic.inc
new file mode 100644
index 0000000..b31ebcb
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/RawTopic.inc
@@ -0,0 +1,121 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/RawTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline RawSubscriber::RawSubscriber(
+    NT_Subscriber handle, std::span<const uint8_t> defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue.begin(), defaultValue.end()} {}
+
+inline std::vector<uint8_t> RawSubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline std::vector<uint8_t> RawSubscriber::Get(
+    std::span<const uint8_t> defaultValue) const {
+  return ::nt::GetRaw(m_subHandle, defaultValue);
+}
+
+inline std::span<uint8_t> RawSubscriber::Get(wpi::SmallVectorImpl<uint8_t>& buf) const {
+  return Get(buf, m_defaultValue);
+}
+
+inline std::span<uint8_t> RawSubscriber::Get(wpi::SmallVectorImpl<uint8_t>& buf, std::span<const uint8_t> defaultValue) const {
+  return nt::GetRaw(m_subHandle, buf, defaultValue);
+}
+
+inline TimestampedRaw RawSubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedRaw RawSubscriber::GetAtomic(
+    std::span<const uint8_t> defaultValue) const {
+  return ::nt::GetAtomicRaw(m_subHandle, defaultValue);
+}
+
+inline TimestampedRawView RawSubscriber::GetAtomic(wpi::SmallVectorImpl<uint8_t>& buf) const {
+  return GetAtomic(buf, m_defaultValue);
+}
+
+inline TimestampedRawView RawSubscriber::GetAtomic(wpi::SmallVectorImpl<uint8_t>& buf, std::span<const uint8_t> defaultValue) const {
+  return nt::GetAtomicRaw(m_subHandle, buf, defaultValue);
+}
+
+inline std::vector<TimestampedRaw>
+RawSubscriber::ReadQueue() {
+  return ::nt::ReadQueueRaw(m_subHandle);
+}
+
+inline RawTopic RawSubscriber::GetTopic() const {
+  return RawTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline RawPublisher::RawPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void RawPublisher::Set(std::span<const uint8_t> value,
+                                         int64_t time) {
+  ::nt::SetRaw(m_pubHandle, value, time);
+}
+
+inline void RawPublisher::SetDefault(std::span<const uint8_t> value) {
+  ::nt::SetDefaultRaw(m_pubHandle, value);
+}
+
+inline RawTopic RawPublisher::GetTopic() const {
+  return RawTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline RawEntry::RawEntry(
+    NT_Entry handle, std::span<const uint8_t> defaultValue)
+    : RawSubscriber{handle, defaultValue},
+      RawPublisher{handle} {}
+
+inline RawTopic RawEntry::GetTopic() const {
+  return RawTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void RawEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline RawSubscriber RawTopic::Subscribe(
+    std::string_view typeString, std::span<const uint8_t> defaultValue,
+    const PubSubOptions& options) {
+  return RawSubscriber{
+      ::nt::Subscribe(m_handle, NT_RAW, typeString, options),
+      defaultValue};
+}
+inline RawPublisher RawTopic::Publish(
+    std::string_view typeString, const PubSubOptions& options) {
+  return RawPublisher{
+      ::nt::Publish(m_handle, NT_RAW, typeString, options)};
+}
+
+inline RawPublisher RawTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return RawPublisher{
+      ::nt::PublishEx(m_handle, NT_RAW, typeString, properties, options)};
+}
+
+inline RawEntry RawTopic::GetEntry(
+    std::string_view typeString, std::span<const uint8_t> defaultValue,
+    const PubSubOptions& options) {
+  return RawEntry{
+      ::nt::GetEntry(m_handle, NT_RAW, typeString, options),
+      defaultValue};
+}
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/StringArrayTopic.h
similarity index 64%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/StringArrayTopic.h
index ec2a915..9339521 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/StringArrayTopic.h
@@ -2,11 +2,13 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+#include <utility>
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +24,29 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class StringArrayTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables StringArray subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class StringArraySubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = StringArrayTopic;
+  using ValueType = std::vector<std::string>;
+  using ParamType = std::span<const std::string>;
+  using TimestampedValueType = TimestampedStringArray;
 
-  {{ TypeName }}Subscriber() = default;
+
+  StringArraySubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * StringArrayTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  StringArraySubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,27 +64,8 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value.
-   * If no value has been published, returns the stored default value.
-   *
-   * @param buf storage for returned value
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value.
-   * If no value has been published, returns the passed defaultValue.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return value
-   */
-  SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
    * timestamp of 0.
@@ -104,32 +83,8 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the stored default value and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf) const;
 
   /**
-   * Get the last published value along with its timestamp.
-   * If no value has been published, returns the passed defaultValue and a
-   * timestamp of 0.
-   *
-   * @param buf storage for returned value
-   * @param defaultValue default value to return if no value has been published
-   * @return timestamped value
-   */
-  TimestampedValueViewType GetAtomic(
-      wpi::SmallVectorImpl<SmallElemType>& buf,
-      ParamType defaultValue) const;
-{% endif %}
-  /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
    *
@@ -153,28 +108,25 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables StringArray publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class StringArrayPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = StringArrayTopic;
+  using ValueType = std::vector<std::string>;
+  using ParamType = std::span<const std::string>;
 
-  {{ TypeName }}Publisher() = default;
+  using TimestampedValueType = TimestampedStringArray;
+
+  StringArrayPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * StringArrayTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit StringArrayPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +154,31 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables StringArray entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class StringArrayEntry final : public StringArraySubscriber,
+                                  public StringArrayPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = StringArraySubscriber;
+  using PublisherType = StringArrayPublisher;
+  using TopicType = StringArrayTopic;
+  using ValueType = std::vector<std::string>;
+  using ParamType = std::span<const std::string>;
 
-  {{ TypeName }}Entry() = default;
+  using TimestampedValueType = TimestampedStringArray;
+
+  StringArrayEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * StringArrayTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  StringArrayEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +208,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables StringArray topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class StringArrayTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = StringArraySubscriber;
+  using PublisherType = StringArrayPublisher;
+  using EntryType = StringArrayEntry;
+  using ValueType = std::vector<std::string>;
+  using ParamType = std::span<const std::string>;
+  using TimestampedValueType = TimestampedStringArray;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "string[]";
 
-  {{ TypeName }}Topic() = default;
+  StringArrayTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetStringArrayTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit StringArrayTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit StringArrayTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +248,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +255,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +277,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +290,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +333,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +365,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/StringArrayTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/StringArrayTopic.inc b/ntcore/src/generated/main/native/include/networktables/StringArrayTopic.inc
new file mode 100644
index 0000000..f003ff0
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/StringArrayTopic.inc
@@ -0,0 +1,121 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/StringArrayTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline StringArraySubscriber::StringArraySubscriber(
+    NT_Subscriber handle, std::span<const std::string> defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue.begin(), defaultValue.end()} {}
+
+inline std::vector<std::string> StringArraySubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline std::vector<std::string> StringArraySubscriber::Get(
+    std::span<const std::string> defaultValue) const {
+  return ::nt::GetStringArray(m_subHandle, defaultValue);
+}
+
+inline TimestampedStringArray StringArraySubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedStringArray StringArraySubscriber::GetAtomic(
+    std::span<const std::string> defaultValue) const {
+  return ::nt::GetAtomicStringArray(m_subHandle, defaultValue);
+}
+
+inline std::vector<TimestampedStringArray>
+StringArraySubscriber::ReadQueue() {
+  return ::nt::ReadQueueStringArray(m_subHandle);
+}
+
+inline StringArrayTopic StringArraySubscriber::GetTopic() const {
+  return StringArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline StringArrayPublisher::StringArrayPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void StringArrayPublisher::Set(std::span<const std::string> value,
+                                         int64_t time) {
+  ::nt::SetStringArray(m_pubHandle, value, time);
+}
+
+inline void StringArrayPublisher::SetDefault(std::span<const std::string> value) {
+  ::nt::SetDefaultStringArray(m_pubHandle, value);
+}
+
+inline StringArrayTopic StringArrayPublisher::GetTopic() const {
+  return StringArrayTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline StringArrayEntry::StringArrayEntry(
+    NT_Entry handle, std::span<const std::string> defaultValue)
+    : StringArraySubscriber{handle, defaultValue},
+      StringArrayPublisher{handle} {}
+
+inline StringArrayTopic StringArrayEntry::GetTopic() const {
+  return StringArrayTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void StringArrayEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline StringArraySubscriber StringArrayTopic::Subscribe(
+    std::span<const std::string> defaultValue,
+    const PubSubOptions& options) {
+  return StringArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_STRING_ARRAY, "string[]", options),
+      defaultValue};
+}
+inline StringArraySubscriber StringArrayTopic::SubscribeEx(
+    std::string_view typeString, std::span<const std::string> defaultValue,
+    const PubSubOptions& options) {
+  return StringArraySubscriber{
+      ::nt::Subscribe(m_handle, NT_STRING_ARRAY, typeString, options),
+      defaultValue};
+}
+
+inline StringArrayPublisher StringArrayTopic::Publish(
+    const PubSubOptions& options) {
+  return StringArrayPublisher{
+      ::nt::Publish(m_handle, NT_STRING_ARRAY, "string[]", options)};
+}
+
+inline StringArrayPublisher StringArrayTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return StringArrayPublisher{
+      ::nt::PublishEx(m_handle, NT_STRING_ARRAY, typeString, properties, options)};
+}
+
+inline StringArrayEntry StringArrayTopic::GetEntry(
+    std::span<const std::string> defaultValue,
+    const PubSubOptions& options) {
+  return StringArrayEntry{
+      ::nt::GetEntry(m_handle, NT_STRING_ARRAY, "string[]", options),
+      defaultValue};
+}
+inline StringArrayEntry StringArrayTopic::GetEntryEx(
+    std::string_view typeString, std::span<const std::string> defaultValue,
+    const PubSubOptions& options) {
+  return StringArrayEntry{
+      ::nt::GetEntry(m_handle, NT_STRING_ARRAY, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generated/main/native/include/networktables/StringTopic.h
similarity index 74%
copy from ntcore/src/generate/include/networktables/Topic.h.jinja
copy to ntcore/src/generated/main/native/include/networktables/StringTopic.h
index ec2a915..3e0ff87 100644
--- a/ntcore/src/generate/include/networktables/Topic.h.jinja
+++ b/ntcore/src/generated/main/native/include/networktables/StringTopic.h
@@ -2,11 +2,15 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
 #pragma once
 
 #include <stdint.h>
 
-{{ cpp.INCLUDES }}
+#include <string>
+#include <string_view>
+#include <utility>
 #include <span>
 #include <string_view>
 #include <vector>
@@ -22,33 +26,33 @@
 
 namespace nt {
 
-class {{ TypeName }}Topic;
+class StringTopic;
 
 /**
- * NetworkTables {{ TypeName }} subscriber.
+ * NetworkTables String subscriber.
  */
-class {{ TypeName }}Subscriber : public Subscriber {
+class StringSubscriber : public Subscriber {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-  using TimestampedValueViewType = Timestamped{{ TypeName }}View;
-{% endif %}
+  using TopicType = StringTopic;
+  using ValueType = std::string;
+  using ParamType = std::string_view;
+  using TimestampedValueType = TimestampedString;
 
-  {{ TypeName }}Subscriber() = default;
+  using SmallRetType = std::string_view;
+  using SmallElemType = char;
+  using TimestampedValueViewType = TimestampedStringView;
+
+
+  StringSubscriber() = default;
 
   /**
    * Construct from a subscriber handle; recommended to use
-   * {{TypeName}}Topic::Subscribe() instead.
+   * StringTopic::Subscribe() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+  StringSubscriber(NT_Subscriber handle, ParamType defaultValue);
 
   /**
    * Get the last published value.
@@ -66,7 +70,7 @@
    * @return value
    */
   ValueType Get(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value.
    * If no value has been published, returns the stored default value.
@@ -85,7 +89,7 @@
    * @return value
    */
   SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get the last published value along with its timestamp
    * If no value has been published, returns the stored default value and a
@@ -104,7 +108,7 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(ParamType defaultValue) const;
-{% if cpp.SmallRetType and cpp.SmallElemType %}
+
   /**
    * Get the last published value along with its timestamp.
    * If no value has been published, returns the stored default value and a
@@ -128,7 +132,7 @@
   TimestampedValueViewType GetAtomic(
       wpi::SmallVectorImpl<SmallElemType>& buf,
       ParamType defaultValue) const;
-{% endif %}
+
   /**
    * Get an array of all value changes since the last call to ReadQueue.
    * Also provides a timestamp for each value.
@@ -153,28 +157,28 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} publisher.
+ * NetworkTables String publisher.
  */
-class {{ TypeName }}Publisher : public Publisher {
+class StringPublisher : public Publisher {
  public:
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using TopicType = StringTopic;
+  using ValueType = std::string;
+  using ParamType = std::string_view;
 
-  {{ TypeName }}Publisher() = default;
+  using SmallRetType = std::string_view;
+  using SmallElemType = char;
+
+  using TimestampedValueType = TimestampedString;
+
+  StringPublisher() = default;
 
   /**
    * Construct from a publisher handle; recommended to use
-   * {{TypeName}}Topic::Publish() instead.
+   * StringTopic::Publish() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Publisher(NT_Publisher handle);
+  explicit StringPublisher(NT_Publisher handle);
 
   /**
    * Publish a new value.
@@ -202,34 +206,34 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} entry.
+ * NetworkTables String entry.
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
-                                  public {{ TypeName }}Publisher {
+class StringEntry final : public StringSubscriber,
+                                  public StringPublisher {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using TopicType = {{ TypeName }}Topic;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-{% if cpp.SmallRetType and cpp.SmallElemType %}
-  using SmallRetType = {{ cpp.SmallRetType }};
-  using SmallElemType = {{ cpp.SmallElemType }};
-{% endif %}
-  using TimestampedValueType = Timestamped{{ TypeName }};
+  using SubscriberType = StringSubscriber;
+  using PublisherType = StringPublisher;
+  using TopicType = StringTopic;
+  using ValueType = std::string;
+  using ParamType = std::string_view;
 
-  {{ TypeName }}Entry() = default;
+  using SmallRetType = std::string_view;
+  using SmallElemType = char;
+
+  using TimestampedValueType = TimestampedString;
+
+  StringEntry() = default;
 
   /**
    * Construct from an entry handle; recommended to use
-   * {{TypeName}}Topic::GetEntry() instead.
+   * StringTopic::GetEntry() instead.
    *
    * @param handle Native handle
    * @param defaultValue Default value
    */
-  {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+  StringEntry(NT_Entry handle, ParamType defaultValue);
 
   /**
    * Determines if the native handle is valid.
@@ -259,37 +263,35 @@
 };
 
 /**
- * NetworkTables {{ TypeName }} topic.
+ * NetworkTables String topic.
  */
-class {{ TypeName }}Topic final : public Topic {
+class StringTopic final : public Topic {
  public:
-  using SubscriberType = {{ TypeName }}Subscriber;
-  using PublisherType = {{ TypeName }}Publisher;
-  using EntryType = {{ TypeName }}Entry;
-  using ValueType = {{ cpp.ValueType }};
-  using ParamType = {{ cpp.ParamType }};
-  using TimestampedValueType = Timestamped{{ TypeName }};
-{%- if TypeString %}
+  using SubscriberType = StringSubscriber;
+  using PublisherType = StringPublisher;
+  using EntryType = StringEntry;
+  using ValueType = std::string;
+  using ParamType = std::string_view;
+  using TimestampedValueType = TimestampedString;
   /** The default type string for this topic type. */
-  static constexpr std::string_view kTypeString = {{ TypeString }};
-{%- endif %}
+  static constexpr std::string_view kTypeString = "string";
 
-  {{ TypeName }}Topic() = default;
+  StringTopic() = default;
 
   /**
    * Construct from a topic handle; recommended to use
-   * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+   * NetworkTableInstance::GetStringTopic() instead.
    *
    * @param handle Native handle
    */
-  explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+  explicit StringTopic(NT_Topic handle) : Topic{handle} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
    */
-  explicit {{ TypeName }}Topic(Topic topic) : Topic{topic} {}
+  explicit StringTopic(Topic topic) : Topic{topic} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -301,9 +303,6 @@
    *     any values. To determine if the data type matches, use the appropriate
    *     Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options subscribe options
@@ -311,9 +310,8 @@
    */
   [[nodiscard]]
   SubscriberType Subscribe(
-      {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+      ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new subscriber to the topic, with specific type string.
    *
@@ -334,7 +332,7 @@
   SubscriberType SubscribeEx(
       std::string_view typeString, ParamType defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
   /**
    * Create a new publisher to the topic.
    *
@@ -347,14 +345,11 @@
    *     do not match the topic's data type are dropped (ignored). To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param options publish options
    * @return publisher
    */
   [[nodiscard]]
-  PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+  PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions);
 
   /**
    * Create a new publisher to the topic, with type string and initial
@@ -393,18 +388,14 @@
    *     will show no new values if the data type does not match. To determine
    *     if the data type matches, use the appropriate Topic functions.
    *
-{%- if not TypeString %}
-   * @param typeString type string
-{% endif %}
    * @param defaultValue default value used when a default is not provided to a
    *        getter function
    * @param options publish and/or subscribe options
    * @return entry
    */
   [[nodiscard]]
-  EntryType GetEntry({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+  EntryType GetEntry(ParamType defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions);
-{%- if TypeString %}
   /**
    * Create a new entry for the topic, with specific type string.
    *
@@ -429,9 +420,9 @@
   [[nodiscard]]
   EntryType GetEntryEx(std::string_view typeString, ParamType defaultValue,
                        const PubSubOptions& options = kDefaultPubSubOptions);
-{% endif %}
+
 };
 
 }  // namespace nt
 
-#include "networktables/{{ TypeName }}Topic.inc"
+#include "networktables/StringTopic.inc"
diff --git a/ntcore/src/generated/main/native/include/networktables/StringTopic.inc b/ntcore/src/generated/main/native/include/networktables/StringTopic.inc
new file mode 100644
index 0000000..a1934e4
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/networktables/StringTopic.inc
@@ -0,0 +1,137 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <vector>
+
+#include "networktables/StringTopic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline StringSubscriber::StringSubscriber(
+    NT_Subscriber handle, std::string_view defaultValue)
+    : Subscriber{handle},
+      m_defaultValue{defaultValue} {}
+
+inline std::string StringSubscriber::Get() const {
+  return Get(m_defaultValue);
+}
+
+inline std::string StringSubscriber::Get(
+    std::string_view defaultValue) const {
+  return ::nt::GetString(m_subHandle, defaultValue);
+}
+
+inline std::string_view StringSubscriber::Get(wpi::SmallVectorImpl<char>& buf) const {
+  return Get(buf, m_defaultValue);
+}
+
+inline std::string_view StringSubscriber::Get(wpi::SmallVectorImpl<char>& buf, std::string_view defaultValue) const {
+  return nt::GetString(m_subHandle, buf, defaultValue);
+}
+
+inline TimestampedString StringSubscriber::GetAtomic() const {
+  return GetAtomic(m_defaultValue);
+}
+
+inline TimestampedString StringSubscriber::GetAtomic(
+    std::string_view defaultValue) const {
+  return ::nt::GetAtomicString(m_subHandle, defaultValue);
+}
+
+inline TimestampedStringView StringSubscriber::GetAtomic(wpi::SmallVectorImpl<char>& buf) const {
+  return GetAtomic(buf, m_defaultValue);
+}
+
+inline TimestampedStringView StringSubscriber::GetAtomic(wpi::SmallVectorImpl<char>& buf, std::string_view defaultValue) const {
+  return nt::GetAtomicString(m_subHandle, buf, defaultValue);
+}
+
+inline std::vector<TimestampedString>
+StringSubscriber::ReadQueue() {
+  return ::nt::ReadQueueString(m_subHandle);
+}
+
+inline StringTopic StringSubscriber::GetTopic() const {
+  return StringTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline StringPublisher::StringPublisher(NT_Publisher handle)
+    : Publisher{handle} {}
+
+inline void StringPublisher::Set(std::string_view value,
+                                         int64_t time) {
+  ::nt::SetString(m_pubHandle, value, time);
+}
+
+inline void StringPublisher::SetDefault(std::string_view value) {
+  ::nt::SetDefaultString(m_pubHandle, value);
+}
+
+inline StringTopic StringPublisher::GetTopic() const {
+  return StringTopic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline StringEntry::StringEntry(
+    NT_Entry handle, std::string_view defaultValue)
+    : StringSubscriber{handle, defaultValue},
+      StringPublisher{handle} {}
+
+inline StringTopic StringEntry::GetTopic() const {
+  return StringTopic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void StringEntry::Unpublish() {
+  ::nt::Unpublish(m_pubHandle);
+}
+
+inline StringSubscriber StringTopic::Subscribe(
+    std::string_view defaultValue,
+    const PubSubOptions& options) {
+  return StringSubscriber{
+      ::nt::Subscribe(m_handle, NT_STRING, "string", options),
+      defaultValue};
+}
+inline StringSubscriber StringTopic::SubscribeEx(
+    std::string_view typeString, std::string_view defaultValue,
+    const PubSubOptions& options) {
+  return StringSubscriber{
+      ::nt::Subscribe(m_handle, NT_STRING, typeString, options),
+      defaultValue};
+}
+
+inline StringPublisher StringTopic::Publish(
+    const PubSubOptions& options) {
+  return StringPublisher{
+      ::nt::Publish(m_handle, NT_STRING, "string", options)};
+}
+
+inline StringPublisher StringTopic::PublishEx(
+    std::string_view typeString,
+    const wpi::json& properties, const PubSubOptions& options) {
+  return StringPublisher{
+      ::nt::PublishEx(m_handle, NT_STRING, typeString, properties, options)};
+}
+
+inline StringEntry StringTopic::GetEntry(
+    std::string_view defaultValue,
+    const PubSubOptions& options) {
+  return StringEntry{
+      ::nt::GetEntry(m_handle, NT_STRING, "string", options),
+      defaultValue};
+}
+inline StringEntry StringTopic::GetEntryEx(
+    std::string_view typeString, std::string_view defaultValue,
+    const PubSubOptions& options) {
+  return StringEntry{
+      ::nt::GetEntry(m_handle, NT_STRING, typeString, options),
+      defaultValue};
+}
+
+}  // namespace nt
diff --git a/ntcore/src/generated/main/native/include/ntcore_c_types.h b/ntcore/src/generated/main/native/include/ntcore_c_types.h
new file mode 100644
index 0000000..3b95a09
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/ntcore_c_types.h
@@ -0,0 +1,1245 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <stdint.h>
+
+#include "ntcore_c.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/**
+ * Timestamped Boolean.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedBoolean {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  NT_Bool value;
+};
+
+/**
+ * @defgroup ntcore_Boolean_cfunc Boolean Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ */
+NT_Bool NT_SetBoolean(NT_Handle pubentry, int64_t time, NT_Bool value);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+NT_Bool NT_SetDefaultBoolean(NT_Handle pubentry, NT_Bool defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+NT_Bool NT_GetBoolean(NT_Handle subentry, NT_Bool defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicBoolean(NT_Handle subentry, NT_Bool defaultValue, struct NT_TimestampedBoolean* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicBoolean).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedBoolean(struct NT_TimestampedBoolean* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedBoolean* NT_ReadQueueBoolean(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueBoolean).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueBoolean(struct NT_TimestampedBoolean* arr, size_t len);
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+NT_Bool* NT_ReadQueueValuesBoolean(NT_Handle subentry, size_t* len);
+
+/** @} */
+
+/**
+ * Timestamped Integer.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedInteger {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  int64_t value;
+};
+
+/**
+ * @defgroup ntcore_Integer_cfunc Integer Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ */
+NT_Bool NT_SetInteger(NT_Handle pubentry, int64_t time, int64_t value);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+NT_Bool NT_SetDefaultInteger(NT_Handle pubentry, int64_t defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+int64_t NT_GetInteger(NT_Handle subentry, int64_t defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicInteger(NT_Handle subentry, int64_t defaultValue, struct NT_TimestampedInteger* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicInteger).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedInteger(struct NT_TimestampedInteger* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedInteger* NT_ReadQueueInteger(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueInteger).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueInteger(struct NT_TimestampedInteger* arr, size_t len);
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+int64_t* NT_ReadQueueValuesInteger(NT_Handle subentry, size_t* len);
+
+/** @} */
+
+/**
+ * Timestamped Float.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedFloat {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  float value;
+};
+
+/**
+ * @defgroup ntcore_Float_cfunc Float Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ */
+NT_Bool NT_SetFloat(NT_Handle pubentry, int64_t time, float value);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+NT_Bool NT_SetDefaultFloat(NT_Handle pubentry, float defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+float NT_GetFloat(NT_Handle subentry, float defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicFloat(NT_Handle subentry, float defaultValue, struct NT_TimestampedFloat* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicFloat).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedFloat(struct NT_TimestampedFloat* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedFloat* NT_ReadQueueFloat(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueFloat).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueFloat(struct NT_TimestampedFloat* arr, size_t len);
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+float* NT_ReadQueueValuesFloat(NT_Handle subentry, size_t* len);
+
+/** @} */
+
+/**
+ * Timestamped Double.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedDouble {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  double value;
+};
+
+/**
+ * @defgroup ntcore_Double_cfunc Double Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ */
+NT_Bool NT_SetDouble(NT_Handle pubentry, int64_t time, double value);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+NT_Bool NT_SetDefaultDouble(NT_Handle pubentry, double defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+double NT_GetDouble(NT_Handle subentry, double defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicDouble(NT_Handle subentry, double defaultValue, struct NT_TimestampedDouble* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicDouble).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedDouble(struct NT_TimestampedDouble* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedDouble* NT_ReadQueueDouble(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueDouble).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueDouble(struct NT_TimestampedDouble* arr, size_t len);
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+double* NT_ReadQueueValuesDouble(NT_Handle subentry, size_t* len);
+
+/** @} */
+
+/**
+ * Timestamped String.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedString {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  char* value;
+  /**
+   * Value length.
+   */
+  size_t len;
+
+};
+
+/**
+ * @defgroup ntcore_String_cfunc String Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ * @param len length of value
+
+ */
+NT_Bool NT_SetString(NT_Handle pubentry, int64_t time, const char* value, size_t len);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ * @param defaultValueLen length of default value
+
+ */
+NT_Bool NT_SetDefaultString(NT_Handle pubentry, const char* defaultValue, size_t defaultValueLen);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+ * @param len length of returned value (output)
+
+ * @return value
+ */
+char* NT_GetString(NT_Handle subentry, const char* defaultValue, size_t defaultValueLen, size_t* len);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicString(NT_Handle subentry, const char* defaultValue, size_t defaultValueLen, struct NT_TimestampedString* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicString).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedString(struct NT_TimestampedString* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedString* NT_ReadQueueString(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueString).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueString(struct NT_TimestampedString* arr, size_t len);
+
+/** @} */
+
+/**
+ * Timestamped Raw.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedRaw {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  uint8_t* value;
+  /**
+   * Value length.
+   */
+  size_t len;
+
+};
+
+/**
+ * @defgroup ntcore_Raw_cfunc Raw Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ * @param len length of value
+
+ */
+NT_Bool NT_SetRaw(NT_Handle pubentry, int64_t time, const uint8_t* value, size_t len);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ * @param defaultValueLen length of default value
+
+ */
+NT_Bool NT_SetDefaultRaw(NT_Handle pubentry, const uint8_t* defaultValue, size_t defaultValueLen);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+ * @param len length of returned value (output)
+
+ * @return value
+ */
+uint8_t* NT_GetRaw(NT_Handle subentry, const uint8_t* defaultValue, size_t defaultValueLen, size_t* len);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicRaw(NT_Handle subentry, const uint8_t* defaultValue, size_t defaultValueLen, struct NT_TimestampedRaw* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicRaw).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedRaw(struct NT_TimestampedRaw* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedRaw* NT_ReadQueueRaw(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueRaw).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueRaw(struct NT_TimestampedRaw* arr, size_t len);
+
+/** @} */
+
+/**
+ * Timestamped BooleanArray.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedBooleanArray {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  NT_Bool* value;
+  /**
+   * Value length.
+   */
+  size_t len;
+
+};
+
+/**
+ * @defgroup ntcore_BooleanArray_cfunc BooleanArray Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ * @param len length of value
+
+ */
+NT_Bool NT_SetBooleanArray(NT_Handle pubentry, int64_t time, const NT_Bool* value, size_t len);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ * @param defaultValueLen length of default value
+
+ */
+NT_Bool NT_SetDefaultBooleanArray(NT_Handle pubentry, const NT_Bool* defaultValue, size_t defaultValueLen);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+ * @param len length of returned value (output)
+
+ * @return value
+ */
+NT_Bool* NT_GetBooleanArray(NT_Handle subentry, const NT_Bool* defaultValue, size_t defaultValueLen, size_t* len);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicBooleanArray(NT_Handle subentry, const NT_Bool* defaultValue, size_t defaultValueLen, struct NT_TimestampedBooleanArray* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicBooleanArray).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedBooleanArray(struct NT_TimestampedBooleanArray* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedBooleanArray* NT_ReadQueueBooleanArray(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueBooleanArray).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueBooleanArray(struct NT_TimestampedBooleanArray* arr, size_t len);
+
+/** @} */
+
+/**
+ * Timestamped IntegerArray.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedIntegerArray {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  int64_t* value;
+  /**
+   * Value length.
+   */
+  size_t len;
+
+};
+
+/**
+ * @defgroup ntcore_IntegerArray_cfunc IntegerArray Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ * @param len length of value
+
+ */
+NT_Bool NT_SetIntegerArray(NT_Handle pubentry, int64_t time, const int64_t* value, size_t len);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ * @param defaultValueLen length of default value
+
+ */
+NT_Bool NT_SetDefaultIntegerArray(NT_Handle pubentry, const int64_t* defaultValue, size_t defaultValueLen);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+ * @param len length of returned value (output)
+
+ * @return value
+ */
+int64_t* NT_GetIntegerArray(NT_Handle subentry, const int64_t* defaultValue, size_t defaultValueLen, size_t* len);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicIntegerArray(NT_Handle subentry, const int64_t* defaultValue, size_t defaultValueLen, struct NT_TimestampedIntegerArray* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicIntegerArray).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedIntegerArray(struct NT_TimestampedIntegerArray* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedIntegerArray* NT_ReadQueueIntegerArray(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueIntegerArray).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueIntegerArray(struct NT_TimestampedIntegerArray* arr, size_t len);
+
+/** @} */
+
+/**
+ * Timestamped FloatArray.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedFloatArray {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  float* value;
+  /**
+   * Value length.
+   */
+  size_t len;
+
+};
+
+/**
+ * @defgroup ntcore_FloatArray_cfunc FloatArray Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ * @param len length of value
+
+ */
+NT_Bool NT_SetFloatArray(NT_Handle pubentry, int64_t time, const float* value, size_t len);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ * @param defaultValueLen length of default value
+
+ */
+NT_Bool NT_SetDefaultFloatArray(NT_Handle pubentry, const float* defaultValue, size_t defaultValueLen);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+ * @param len length of returned value (output)
+
+ * @return value
+ */
+float* NT_GetFloatArray(NT_Handle subentry, const float* defaultValue, size_t defaultValueLen, size_t* len);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicFloatArray(NT_Handle subentry, const float* defaultValue, size_t defaultValueLen, struct NT_TimestampedFloatArray* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicFloatArray).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedFloatArray(struct NT_TimestampedFloatArray* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedFloatArray* NT_ReadQueueFloatArray(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueFloatArray).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueFloatArray(struct NT_TimestampedFloatArray* arr, size_t len);
+
+/** @} */
+
+/**
+ * Timestamped DoubleArray.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedDoubleArray {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  double* value;
+  /**
+   * Value length.
+   */
+  size_t len;
+
+};
+
+/**
+ * @defgroup ntcore_DoubleArray_cfunc DoubleArray Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ * @param len length of value
+
+ */
+NT_Bool NT_SetDoubleArray(NT_Handle pubentry, int64_t time, const double* value, size_t len);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ * @param defaultValueLen length of default value
+
+ */
+NT_Bool NT_SetDefaultDoubleArray(NT_Handle pubentry, const double* defaultValue, size_t defaultValueLen);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+ * @param len length of returned value (output)
+
+ * @return value
+ */
+double* NT_GetDoubleArray(NT_Handle subentry, const double* defaultValue, size_t defaultValueLen, size_t* len);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicDoubleArray(NT_Handle subentry, const double* defaultValue, size_t defaultValueLen, struct NT_TimestampedDoubleArray* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicDoubleArray).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedDoubleArray(struct NT_TimestampedDoubleArray* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedDoubleArray* NT_ReadQueueDoubleArray(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueDoubleArray).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueDoubleArray(struct NT_TimestampedDoubleArray* arr, size_t len);
+
+/** @} */
+
+/**
+ * Timestamped StringArray.
+ * @ingroup ntcore_c_api
+ */
+struct NT_TimestampedStringArray {
+  /**
+   * Time in local time base.
+   */
+  int64_t time;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime;
+
+  /**
+   * Value.
+   */
+  struct NT_String* value;
+  /**
+   * Value length.
+   */
+  size_t len;
+
+};
+
+/**
+ * @defgroup ntcore_StringArray_cfunc StringArray Functions
+ * @ingroup ntcore_c_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+ * @param len length of value
+
+ */
+NT_Bool NT_SetStringArray(NT_Handle pubentry, int64_t time, const struct NT_String* value, size_t len);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ * @param defaultValueLen length of default value
+
+ */
+NT_Bool NT_SetDefaultStringArray(NT_Handle pubentry, const struct NT_String* defaultValue, size_t defaultValueLen);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+ * @param len length of returned value (output)
+
+ * @return value
+ */
+struct NT_String* NT_GetStringArray(NT_Handle subentry, const struct NT_String* defaultValue, size_t defaultValueLen, size_t* len);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @param defaultValueLen length of default value
+
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomicStringArray(NT_Handle subentry, const struct NT_String* defaultValue, size_t defaultValueLen, struct NT_TimestampedStringArray* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomicStringArray).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestampedStringArray(struct NT_TimestampedStringArray* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ *     been published since the previous call.
+ */
+struct NT_TimestampedStringArray* NT_ReadQueueStringArray(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueueStringArray).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueueStringArray(struct NT_TimestampedStringArray* arr, size_t len);
+
+/** @} */
+
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
diff --git a/ntcore/src/generated/main/native/include/ntcore_cpp_types.h b/ntcore/src/generated/main/native/include/ntcore_cpp_types.h
new file mode 100644
index 0000000..c0cac5e
--- /dev/null
+++ b/ntcore/src/generated/main/native/include/ntcore_cpp_types.h
@@ -0,0 +1,998 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
+
+#pragma once
+
+#include <stdint.h>
+
+#include <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "ntcore_c.h"
+
+namespace wpi {
+template <typename T>
+class SmallVectorImpl;
+}  // namespace wpi
+
+namespace nt {
+/**
+ * Timestamped value.
+ * @ingroup ntcore_cpp_handle_api
+ */
+template <typename T>
+struct Timestamped {
+  Timestamped() = default;
+  Timestamped(int64_t time, int64_t serverTime, T value)
+    : time{time}, serverTime{serverTime}, value{std::move(value)} {}
+
+  /**
+   * Time in local time base.
+   */
+  int64_t time = 0;
+
+  /**
+   * Time in server time base.  May be 0 or 1 for locally set values.
+   */
+  int64_t serverTime = 0;
+
+  /**
+   * Value.
+   */
+  T value = {};
+};
+
+/**
+ * Timestamped Boolean.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedBoolean = Timestamped<bool>;
+
+/**
+ * @defgroup ntcore_Boolean_func Boolean Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetBoolean(NT_Handle pubentry, bool value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultBoolean(NT_Handle pubentry, bool defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+bool GetBoolean(NT_Handle subentry, bool defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedBoolean GetAtomicBoolean(NT_Handle subentry, bool defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedBoolean> ReadQueueBoolean(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<int> ReadQueueValuesBoolean(NT_Handle subentry);
+
+/** @} */
+
+/**
+ * Timestamped Integer.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedInteger = Timestamped<int64_t>;
+
+/**
+ * @defgroup ntcore_Integer_func Integer Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetInteger(NT_Handle pubentry, int64_t value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultInteger(NT_Handle pubentry, int64_t defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+int64_t GetInteger(NT_Handle subentry, int64_t defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedInteger GetAtomicInteger(NT_Handle subentry, int64_t defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedInteger> ReadQueueInteger(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<int64_t> ReadQueueValuesInteger(NT_Handle subentry);
+
+/** @} */
+
+/**
+ * Timestamped Float.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedFloat = Timestamped<float>;
+
+/**
+ * @defgroup ntcore_Float_func Float Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetFloat(NT_Handle pubentry, float value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultFloat(NT_Handle pubentry, float defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+float GetFloat(NT_Handle subentry, float defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedFloat GetAtomicFloat(NT_Handle subentry, float defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedFloat> ReadQueueFloat(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<float> ReadQueueValuesFloat(NT_Handle subentry);
+
+/** @} */
+
+/**
+ * Timestamped Double.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedDouble = Timestamped<double>;
+
+/**
+ * @defgroup ntcore_Double_func Double Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetDouble(NT_Handle pubentry, double value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultDouble(NT_Handle pubentry, double defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+double GetDouble(NT_Handle subentry, double defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedDouble GetAtomicDouble(NT_Handle subentry, double defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedDouble> ReadQueueDouble(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<double> ReadQueueValuesDouble(NT_Handle subentry);
+
+/** @} */
+
+/**
+ * Timestamped String.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedString = Timestamped<std::string>;
+
+/**
+ * Timestamped String view (for SmallVector-taking functions).
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedStringView = Timestamped<std::string_view>;
+
+/**
+ * @defgroup ntcore_String_func String Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetString(NT_Handle pubentry, std::string_view value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultString(NT_Handle pubentry, std::string_view defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+std::string GetString(NT_Handle subentry, std::string_view defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedString GetAtomicString(NT_Handle subentry, std::string_view defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedString> ReadQueueString(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<std::string> ReadQueueValuesString(NT_Handle subentry);
+
+std::string_view GetString(NT_Handle subentry, wpi::SmallVectorImpl<char>& buf, std::string_view defaultValue);
+
+TimestampedStringView GetAtomicString(
+      NT_Handle subentry,
+      wpi::SmallVectorImpl<char>& buf,
+      std::string_view defaultValue);
+
+/** @} */
+
+/**
+ * Timestamped Raw.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedRaw = Timestamped<std::vector<uint8_t>>;
+
+/**
+ * Timestamped Raw view (for SmallVector-taking functions).
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedRawView = Timestamped<std::span<uint8_t>>;
+
+/**
+ * @defgroup ntcore_Raw_func Raw Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetRaw(NT_Handle pubentry, std::span<const uint8_t> value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultRaw(NT_Handle pubentry, std::span<const uint8_t> defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+std::vector<uint8_t> GetRaw(NT_Handle subentry, std::span<const uint8_t> defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedRaw GetAtomicRaw(NT_Handle subentry, std::span<const uint8_t> defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedRaw> ReadQueueRaw(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<std::vector<uint8_t>> ReadQueueValuesRaw(NT_Handle subentry);
+
+std::span<uint8_t> GetRaw(NT_Handle subentry, wpi::SmallVectorImpl<uint8_t>& buf, std::span<const uint8_t> defaultValue);
+
+TimestampedRawView GetAtomicRaw(
+      NT_Handle subentry,
+      wpi::SmallVectorImpl<uint8_t>& buf,
+      std::span<const uint8_t> defaultValue);
+
+/** @} */
+
+/**
+ * Timestamped BooleanArray.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedBooleanArray = Timestamped<std::vector<int>>;
+
+/**
+ * Timestamped BooleanArray view (for SmallVector-taking functions).
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedBooleanArrayView = Timestamped<std::span<int>>;
+
+/**
+ * @defgroup ntcore_BooleanArray_func BooleanArray Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetBooleanArray(NT_Handle pubentry, std::span<const int> value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultBooleanArray(NT_Handle pubentry, std::span<const int> defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+std::vector<int> GetBooleanArray(NT_Handle subentry, std::span<const int> defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedBooleanArray GetAtomicBooleanArray(NT_Handle subentry, std::span<const int> defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedBooleanArray> ReadQueueBooleanArray(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<std::vector<int>> ReadQueueValuesBooleanArray(NT_Handle subentry);
+
+std::span<int> GetBooleanArray(NT_Handle subentry, wpi::SmallVectorImpl<int>& buf, std::span<const int> defaultValue);
+
+TimestampedBooleanArrayView GetAtomicBooleanArray(
+      NT_Handle subentry,
+      wpi::SmallVectorImpl<int>& buf,
+      std::span<const int> defaultValue);
+
+/** @} */
+
+/**
+ * Timestamped IntegerArray.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedIntegerArray = Timestamped<std::vector<int64_t>>;
+
+/**
+ * Timestamped IntegerArray view (for SmallVector-taking functions).
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedIntegerArrayView = Timestamped<std::span<int64_t>>;
+
+/**
+ * @defgroup ntcore_IntegerArray_func IntegerArray Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetIntegerArray(NT_Handle pubentry, std::span<const int64_t> value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultIntegerArray(NT_Handle pubentry, std::span<const int64_t> defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+std::vector<int64_t> GetIntegerArray(NT_Handle subentry, std::span<const int64_t> defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedIntegerArray GetAtomicIntegerArray(NT_Handle subentry, std::span<const int64_t> defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedIntegerArray> ReadQueueIntegerArray(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<std::vector<int64_t>> ReadQueueValuesIntegerArray(NT_Handle subentry);
+
+std::span<int64_t> GetIntegerArray(NT_Handle subentry, wpi::SmallVectorImpl<int64_t>& buf, std::span<const int64_t> defaultValue);
+
+TimestampedIntegerArrayView GetAtomicIntegerArray(
+      NT_Handle subentry,
+      wpi::SmallVectorImpl<int64_t>& buf,
+      std::span<const int64_t> defaultValue);
+
+/** @} */
+
+/**
+ * Timestamped FloatArray.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedFloatArray = Timestamped<std::vector<float>>;
+
+/**
+ * Timestamped FloatArray view (for SmallVector-taking functions).
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedFloatArrayView = Timestamped<std::span<float>>;
+
+/**
+ * @defgroup ntcore_FloatArray_func FloatArray Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetFloatArray(NT_Handle pubentry, std::span<const float> value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultFloatArray(NT_Handle pubentry, std::span<const float> defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+std::vector<float> GetFloatArray(NT_Handle subentry, std::span<const float> defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedFloatArray GetAtomicFloatArray(NT_Handle subentry, std::span<const float> defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedFloatArray> ReadQueueFloatArray(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<std::vector<float>> ReadQueueValuesFloatArray(NT_Handle subentry);
+
+std::span<float> GetFloatArray(NT_Handle subentry, wpi::SmallVectorImpl<float>& buf, std::span<const float> defaultValue);
+
+TimestampedFloatArrayView GetAtomicFloatArray(
+      NT_Handle subentry,
+      wpi::SmallVectorImpl<float>& buf,
+      std::span<const float> defaultValue);
+
+/** @} */
+
+/**
+ * Timestamped DoubleArray.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedDoubleArray = Timestamped<std::vector<double>>;
+
+/**
+ * Timestamped DoubleArray view (for SmallVector-taking functions).
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedDoubleArrayView = Timestamped<std::span<double>>;
+
+/**
+ * @defgroup ntcore_DoubleArray_func DoubleArray Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetDoubleArray(NT_Handle pubentry, std::span<const double> value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultDoubleArray(NT_Handle pubentry, std::span<const double> defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+std::vector<double> GetDoubleArray(NT_Handle subentry, std::span<const double> defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedDoubleArray GetAtomicDoubleArray(NT_Handle subentry, std::span<const double> defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedDoubleArray> ReadQueueDoubleArray(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<std::vector<double>> ReadQueueValuesDoubleArray(NT_Handle subentry);
+
+std::span<double> GetDoubleArray(NT_Handle subentry, wpi::SmallVectorImpl<double>& buf, std::span<const double> defaultValue);
+
+TimestampedDoubleArrayView GetAtomicDoubleArray(
+      NT_Handle subentry,
+      wpi::SmallVectorImpl<double>& buf,
+      std::span<const double> defaultValue);
+
+/** @} */
+
+/**
+ * Timestamped StringArray.
+ * @ingroup ntcore_cpp_handle_api
+ */
+using TimestampedStringArray = Timestamped<std::vector<std::string>>;
+
+/**
+ * @defgroup ntcore_StringArray_func StringArray Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool SetStringArray(NT_Handle pubentry, std::span<const std::string> value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefaultStringArray(NT_Handle pubentry, std::span<const std::string> defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+std::vector<std::string> GetStringArray(NT_Handle subentry, std::span<const std::string> defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+TimestampedStringArray GetAtomicStringArray(NT_Handle subentry, std::span<const std::string> defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<TimestampedStringArray> ReadQueueStringArray(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ *     depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ *     been published since the previous call.
+ */
+std::vector<std::vector<std::string>> ReadQueueValuesStringArray(NT_Handle subentry);
+
+/** @} */
+
+}  // namespace nt
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java
index 3c65938..c88ce8b 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java
@@ -574,7 +574,7 @@
     return m_inst.addListener(
         new String[] {m_pathWithSep},
         EnumSet.of(NetworkTableEvent.Kind.kPublish, NetworkTableEvent.Kind.kImmediate),
-        new Consumer<NetworkTableEvent>() {
+        new Consumer<>() {
           final Set<String> m_notifiedTables = new HashSet<>();
 
           @Override
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java
index 2350b49..3fea5a3 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java
@@ -6,17 +6,29 @@
 
 /** Network table data types. */
 public enum NetworkTableType {
+  /** Unassigned data type. */
   kUnassigned(0, ""),
+  /** Boolean data type. */
   kBoolean(0x01, "boolean"),
+  /** Double precision floating-point data type. */
   kDouble(0x02, "double"),
+  /** String data type. */
   kString(0x04, "string"),
+  /** Raw data type. */
   kRaw(0x08, "raw"),
+  /** Boolean array data type. */
   kBooleanArray(0x10, "boolean[]"),
+  /** Double precision floating-point array data type. */
   kDoubleArray(0x20, "double[]"),
+  /** String array data type. */
   kStringArray(0x40, "string[]"),
+  /** Integer data type. */
   kInteger(0x100, "int"),
+  /** Single precision floating-point data type. */
   kFloat(0x200, "float"),
+  /** Integer array data type. */
   kIntegerArray(0x400, "int[]"),
+  /** Single precision floating-point array data type. */
   kFloatArray(0x800, "float[]");
 
   private final int m_value;
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ProtobufEntryImpl.java b/ntcore/src/main/java/edu/wpi/first/networktables/ProtobufEntryImpl.java
index b4359ea..6d891c4 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/ProtobufEntryImpl.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/ProtobufEntryImpl.java
@@ -190,14 +190,14 @@
 
   private TimestampedObject<T> fromRaw(TimestampedRaw raw, T defaultValue) {
     if (raw.value.length == 0) {
-      return new TimestampedObject<T>(0, 0, defaultValue);
+      return new TimestampedObject<>(0, 0, defaultValue);
     }
     try {
       synchronized (m_buf) {
-        return new TimestampedObject<T>(raw.timestamp, raw.serverTime, m_buf.read(raw.value));
+        return new TimestampedObject<>(raw.timestamp, raw.serverTime, m_buf.read(raw.value));
       }
     } catch (IOException e) {
-      return new TimestampedObject<T>(0, 0, defaultValue);
+      return new TimestampedObject<>(0, 0, defaultValue);
     }
   }
 
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ProtobufTopic.java b/ntcore/src/main/java/edu/wpi/first/networktables/ProtobufTopic.java
index c3dad13..15313d7 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/ProtobufTopic.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/ProtobufTopic.java
@@ -32,7 +32,7 @@
    * @return ProtobufTopic for value class
    */
   public static <T> ProtobufTopic<T> wrap(Topic topic, Protobuf<T, ?> proto) {
-    return new ProtobufTopic<T>(topic, proto);
+    return new ProtobufTopic<>(topic, proto);
   }
 
   /**
@@ -47,7 +47,7 @@
    */
   public static <T> ProtobufTopic<T> wrap(
       NetworkTableInstance inst, int handle, Protobuf<T, ?> proto) {
-    return new ProtobufTopic<T>(inst, handle, proto);
+    return new ProtobufTopic<>(inst, handle, proto);
   }
 
   /**
@@ -63,7 +63,7 @@
    * @return subscriber
    */
   public ProtobufSubscriber<T> subscribe(T defaultValue, PubSubOption... options) {
-    return new ProtobufEntryImpl<T>(
+    return new ProtobufEntryImpl<>(
         this,
         ProtobufBuffer.create(m_proto),
         NetworkTablesJNI.subscribe(
@@ -87,7 +87,7 @@
    */
   public ProtobufPublisher<T> publish(PubSubOption... options) {
     m_inst.addSchema(m_proto);
-    return new ProtobufEntryImpl<T>(
+    return new ProtobufEntryImpl<>(
         this,
         ProtobufBuffer.create(m_proto),
         NetworkTablesJNI.publish(
@@ -113,7 +113,7 @@
    */
   public ProtobufPublisher<T> publishEx(String properties, PubSubOption... options) {
     m_inst.addSchema(m_proto);
-    return new ProtobufEntryImpl<T>(
+    return new ProtobufEntryImpl<>(
         this,
         ProtobufBuffer.create(m_proto),
         NetworkTablesJNI.publishEx(
@@ -144,7 +144,7 @@
    * @return entry
    */
   public ProtobufEntry<T> getEntry(T defaultValue, PubSubOption... options) {
-    return new ProtobufEntryImpl<T>(
+    return new ProtobufEntryImpl<>(
         this,
         ProtobufBuffer.create(m_proto),
         NetworkTablesJNI.getEntry(
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/StructArrayEntryImpl.java b/ntcore/src/main/java/edu/wpi/first/networktables/StructArrayEntryImpl.java
index 4e8a4a0..1e6b69e 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/StructArrayEntryImpl.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/StructArrayEntryImpl.java
@@ -177,15 +177,14 @@
   @SuppressWarnings("PMD.AvoidCatchingGenericException")
   private TimestampedObject<T[]> fromRaw(TimestampedRaw raw, T[] defaultValue) {
     if (raw.value.length == 0) {
-      return new TimestampedObject<T[]>(0, 0, defaultValue);
+      return new TimestampedObject<>(0, 0, defaultValue);
     }
     try {
       synchronized (m_buf) {
-        return new TimestampedObject<T[]>(
-            raw.timestamp, raw.serverTime, m_buf.readArray(raw.value));
+        return new TimestampedObject<>(raw.timestamp, raw.serverTime, m_buf.readArray(raw.value));
       }
     } catch (RuntimeException e) {
-      return new TimestampedObject<T[]>(0, 0, defaultValue);
+      return new TimestampedObject<>(0, 0, defaultValue);
     }
   }
 
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/StructArrayTopic.java b/ntcore/src/main/java/edu/wpi/first/networktables/StructArrayTopic.java
index 247501b..557e9dd 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/StructArrayTopic.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/StructArrayTopic.java
@@ -32,7 +32,7 @@
    * @return StructArrayTopic for value class
    */
   public static <T> StructArrayTopic<T> wrap(Topic topic, Struct<T> struct) {
-    return new StructArrayTopic<T>(topic, struct);
+    return new StructArrayTopic<>(topic, struct);
   }
 
   /**
@@ -47,7 +47,7 @@
    */
   public static <T> StructArrayTopic<T> wrap(
       NetworkTableInstance inst, int handle, Struct<T> struct) {
-    return new StructArrayTopic<T>(inst, handle, struct);
+    return new StructArrayTopic<>(inst, handle, struct);
   }
 
   /**
@@ -63,7 +63,7 @@
    * @return subscriber
    */
   public StructArraySubscriber<T> subscribe(T[] defaultValue, PubSubOption... options) {
-    return new StructArrayEntryImpl<T>(
+    return new StructArrayEntryImpl<>(
         this,
         StructBuffer.create(m_struct),
         NetworkTablesJNI.subscribe(
@@ -87,7 +87,7 @@
    */
   public StructArrayPublisher<T> publish(PubSubOption... options) {
     m_inst.addSchema(m_struct);
-    return new StructArrayEntryImpl<T>(
+    return new StructArrayEntryImpl<>(
         this,
         StructBuffer.create(m_struct),
         NetworkTablesJNI.publish(
@@ -113,7 +113,7 @@
    */
   public StructArrayPublisher<T> publishEx(String properties, PubSubOption... options) {
     m_inst.addSchema(m_struct);
-    return new StructArrayEntryImpl<T>(
+    return new StructArrayEntryImpl<>(
         this,
         StructBuffer.create(m_struct),
         NetworkTablesJNI.publishEx(
@@ -144,7 +144,7 @@
    * @return entry
    */
   public StructArrayEntry<T> getEntry(T[] defaultValue, PubSubOption... options) {
-    return new StructArrayEntryImpl<T>(
+    return new StructArrayEntryImpl<>(
         this,
         StructBuffer.create(m_struct),
         NetworkTablesJNI.getEntry(
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/StructEntryImpl.java b/ntcore/src/main/java/edu/wpi/first/networktables/StructEntryImpl.java
index bd02d27..5d37765 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/StructEntryImpl.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/StructEntryImpl.java
@@ -188,14 +188,14 @@
   @SuppressWarnings("PMD.AvoidCatchingGenericException")
   private TimestampedObject<T> fromRaw(TimestampedRaw raw, T defaultValue) {
     if (raw.value.length == 0) {
-      return new TimestampedObject<T>(0, 0, defaultValue);
+      return new TimestampedObject<>(0, 0, defaultValue);
     }
     try {
       synchronized (m_buf) {
-        return new TimestampedObject<T>(raw.timestamp, raw.serverTime, m_buf.read(raw.value));
+        return new TimestampedObject<>(raw.timestamp, raw.serverTime, m_buf.read(raw.value));
       }
     } catch (RuntimeException e) {
-      return new TimestampedObject<T>(0, 0, defaultValue);
+      return new TimestampedObject<>(0, 0, defaultValue);
     }
   }
 
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/StructTopic.java b/ntcore/src/main/java/edu/wpi/first/networktables/StructTopic.java
index b1ff026..22dbd2d 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/StructTopic.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/StructTopic.java
@@ -32,7 +32,7 @@
    * @return StructTopic for value class
    */
   public static <T> StructTopic<T> wrap(Topic topic, Struct<T> struct) {
-    return new StructTopic<T>(topic, struct);
+    return new StructTopic<>(topic, struct);
   }
 
   /**
@@ -46,7 +46,7 @@
    * @return StructTopic for value class
    */
   public static <T> StructTopic<T> wrap(NetworkTableInstance inst, int handle, Struct<T> struct) {
-    return new StructTopic<T>(inst, handle, struct);
+    return new StructTopic<>(inst, handle, struct);
   }
 
   /**
@@ -62,7 +62,7 @@
    * @return subscriber
    */
   public StructSubscriber<T> subscribe(T defaultValue, PubSubOption... options) {
-    return new StructEntryImpl<T>(
+    return new StructEntryImpl<>(
         this,
         StructBuffer.create(m_struct),
         NetworkTablesJNI.subscribe(
@@ -86,7 +86,7 @@
    */
   public StructPublisher<T> publish(PubSubOption... options) {
     m_inst.addSchema(m_struct);
-    return new StructEntryImpl<T>(
+    return new StructEntryImpl<>(
         this,
         StructBuffer.create(m_struct),
         NetworkTablesJNI.publish(
@@ -112,7 +112,7 @@
    */
   public StructPublisher<T> publishEx(String properties, PubSubOption... options) {
     m_inst.addSchema(m_struct);
-    return new StructEntryImpl<T>(
+    return new StructEntryImpl<>(
         this,
         StructBuffer.create(m_struct),
         NetworkTablesJNI.publishEx(
@@ -143,7 +143,7 @@
    * @return entry
    */
   public StructEntry<T> getEntry(T defaultValue, PubSubOption... options) {
-    return new StructEntryImpl<T>(
+    return new StructEntryImpl<>(
         this,
         StructBuffer.create(m_struct),
         NetworkTablesJNI.getEntry(
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TimestampedObject.java b/ntcore/src/main/java/edu/wpi/first/networktables/TimestampedObject.java
index 37c1544..896ec79 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/TimestampedObject.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/TimestampedObject.java
@@ -4,7 +4,11 @@
 
 package edu.wpi.first.networktables;
 
-/** NetworkTables timestamped object. */
+/**
+ * NetworkTables timestamped object.
+ *
+ * @param <T> Value type.
+ */
 public final class TimestampedObject<T> {
   /**
    * Create a timestamped value.
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java b/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java
index db08a34..6794474 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java
@@ -118,6 +118,25 @@
   }
 
   /**
+   * Allow storage of the topic's last value, allowing the value to be read (and not just accessed
+   * through event queues and listeners).
+   *
+   * @param cached True for cached, false for not cached.
+   */
+  public void setCached(boolean cached) {
+    NetworkTablesJNI.setTopicCached(m_handle, cached);
+  }
+
+  /**
+   * Returns whether the topic's last value is stored.
+   *
+   * @return True if the topic is cached.
+   */
+  public boolean isCached() {
+    return NetworkTablesJNI.getTopicCached(m_handle);
+  }
+
+  /**
    * Determines if the topic is currently being published.
    *
    * @return True if the topic exists, false otherwise.
diff --git a/ntcore/src/main/native/cpp/LocalStorage.cpp b/ntcore/src/main/native/cpp/LocalStorage.cpp
index 0377f4f..9cec611 100644
--- a/ntcore/src/main/native/cpp/LocalStorage.cpp
+++ b/ntcore/src/main/native/cpp/LocalStorage.cpp
@@ -130,7 +130,7 @@
     if (!m_dataloggers.empty()) {
       auto now = Now();
       for (auto&& datalogger : m_dataloggers) {
-        if (wpi::starts_with(topic->name, datalogger->prefix)) {
+        if (PrefixMatch(topic->name, datalogger->prefix, topic->special)) {
           auto it = std::find_if(topic->datalogs.begin(), topic->datalogs.end(),
                                  [&](const auto& elem) {
                                    return elem.logger == datalogger->handle;
@@ -175,22 +175,26 @@
 }
 
 bool LocalStorage::Impl::SetValue(TopicData* topic, const Value& value,
-                                  unsigned int eventFlags, bool isDuplicate,
+                                  unsigned int eventFlags,
                                   bool suppressIfDuplicate,
                                   const PublisherData* publisher) {
+  const bool isDuplicate = topic->IsCached() && topic->lastValue == value;
   DEBUG4("SetValue({}, {}, {}, {})", topic->name, value.time(), eventFlags,
          isDuplicate);
   if (topic->type != NT_UNASSIGNED && topic->type != value.type()) {
     return false;
   }
+  // Make sure value isn't older than last value
   if (!topic->lastValue || topic->lastValue.time() == 0 ||
       value.time() >= topic->lastValue.time()) {
     // TODO: notify option even if older value
     if (!(suppressIfDuplicate && isDuplicate)) {
       topic->type = value.type();
-      topic->lastValue = value;
-      topic->lastValueFromNetwork = false;
-      NotifyValue(topic, eventFlags, isDuplicate, publisher);
+      if (topic->IsCached()) {
+        topic->lastValue = value;
+        topic->lastValueFromNetwork = false;
+      }
+      NotifyValue(topic, value, eventFlags, isDuplicate, publisher);
       if (topic->datalogType == value.type()) {
         for (auto&& datalog : topic->datalogs) {
           datalog.Append(value);
@@ -202,8 +206,8 @@
   return true;
 }
 
-void LocalStorage::Impl::NotifyValue(TopicData* topic, unsigned int eventFlags,
-                                     bool isDuplicate,
+void LocalStorage::Impl::NotifyValue(TopicData* topic, const Value& value,
+                                     unsigned int eventFlags, bool isDuplicate,
                                      const PublisherData* publisher) {
   bool isNetwork = (eventFlags & NT_EVENT_VALUE_REMOTE) != 0;
   for (auto&& subscriber : topic->localSubscribers) {
@@ -213,11 +217,11 @@
          (!isNetwork && !subscriber->config.disableLocal)) &&
         (!publisher || (publisher && (subscriber->config.excludePublisher !=
                                       publisher->handle)))) {
-      subscriber->pollStorage.emplace_back(topic->lastValue);
+      subscriber->pollStorage.emplace_back(value);
       subscriber->handle.Set();
       if (!subscriber->valueListeners.empty()) {
         m_listenerStorage.Notify(subscriber->valueListeners, eventFlags,
-                                 topic->handle, 0, topic->lastValue);
+                                 topic->handle, 0, value);
       }
     }
   }
@@ -227,7 +231,7 @@
       subscriber->handle.Set();
       if (!subscriber->valueListeners.empty()) {
         m_listenerStorage.Notify(subscriber->valueListeners, eventFlags,
-                                 topic->handle, 0, topic->lastValue);
+                                 topic->handle, 0, value);
       }
     }
   }
@@ -249,6 +253,22 @@
     topic->properties.erase("retained");
     update["retained"] = wpi::json();
   }
+  if ((flags & NT_UNCACHED) != 0) {
+    topic->properties["cached"] = false;
+    update["cached"] = false;
+  } else {
+    topic->properties.erase("cached");
+    update["cached"] = wpi::json();
+  }
+  if ((flags & NT_UNCACHED) != 0) {
+    topic->lastValue = {};
+    topic->lastValueNetwork = {};
+    topic->lastValueFromNetwork = false;
+  }
+  if ((flags & NT_UNCACHED) != 0 && (flags & NT_PERSISTENT) != 0) {
+    WARN("topic {}: disabling cached property disables persistent storage",
+         topic->name);
+  }
   topic->flags = flags;
   if (!update.empty()) {
     PropertiesUpdated(topic, update, NT_EVENT_NONE, true, false);
@@ -283,6 +303,20 @@
   PropertiesUpdated(topic, update, NT_EVENT_NONE, true, false);
 }
 
+void LocalStorage::Impl::SetCached(TopicData* topic, bool value) {
+  wpi::json update = wpi::json::object();
+  if (value) {
+    topic->flags &= ~NT_UNCACHED;
+    topic->properties.erase("cached");
+    update["cached"] = wpi::json();
+  } else {
+    topic->flags |= NT_UNCACHED;
+    topic->properties["cached"] = false;
+    update["cached"] = false;
+  }
+  PropertiesUpdated(topic, update, NT_EVENT_NONE, true, false);
+}
+
 void LocalStorage::Impl::SetProperties(TopicData* topic,
                                        const wpi::json& update,
                                        bool sendNetwork) {
@@ -328,6 +362,28 @@
         }
       }
     }
+    it = topic->properties.find("cached");
+    if (it != topic->properties.end()) {
+      if (auto val = it->get_ptr<bool*>()) {
+        if (*val) {
+          topic->flags &= ~NT_UNCACHED;
+        } else {
+          topic->flags |= NT_UNCACHED;
+        }
+      }
+    }
+
+    if ((topic->flags & NT_UNCACHED) != 0) {
+      topic->lastValue = {};
+      topic->lastValueNetwork = {};
+      topic->lastValueFromNetwork = false;
+    }
+
+    if ((topic->flags & NT_UNCACHED) != 0 &&
+        (topic->flags & NT_PERSISTENT) != 0) {
+      WARN("topic {}: disabling cached property disables persistent storage",
+           topic->name);
+    }
   }
 
   topic->propertiesStr = topic->properties.dump();
@@ -895,20 +951,22 @@
     return false;
   }
   if (publisher->active) {
-    bool isDuplicate, isNetworkDuplicate, suppressDuplicates;
+    bool isNetworkDuplicate, suppressDuplicates;
     if (force || publisher->config.keepDuplicates) {
       suppressDuplicates = false;
       isNetworkDuplicate = false;
     } else {
       suppressDuplicates = true;
-      isNetworkDuplicate = (publisher->topic->lastValueNetwork == value);
+      isNetworkDuplicate = publisher->topic->IsCached() &&
+                           (publisher->topic->lastValueNetwork == value);
     }
-    isDuplicate = (publisher->topic->lastValue == value);
     if (!isNetworkDuplicate && m_network) {
-      publisher->topic->lastValueNetwork = value;
+      if (publisher->topic->IsCached()) {
+        publisher->topic->lastValueNetwork = value;
+      }
       m_network->SetValue(publisher->handle, value);
     }
-    return SetValue(publisher->topic, value, NT_EVENT_VALUE_LOCAL, isDuplicate,
+    return SetValue(publisher->topic, value, NT_EVENT_VALUE_LOCAL,
                     suppressDuplicates, publisher);
   } else {
     return false;
@@ -940,6 +998,10 @@
     return false;
   }
   if (auto topic = GetTopic(pubsubentryHandle)) {
+    if (!topic->IsCached()) {
+      WARN("ignoring default value on non-cached topic '{}'", topic->name);
+      return false;
+    }
     if (!topic->lastValue &&
         (topic->type == NT_UNASSIGNED || topic->type == value.type() ||
          IsNumericCompatible(topic->type, value.type()))) {
@@ -1026,10 +1088,11 @@
 void LocalStorage::NetworkSetValue(NT_Topic topicHandle, const Value& value) {
   std::scoped_lock lock{m_mutex};
   if (auto topic = m_impl.m_topics.Get(topicHandle)) {
-    if (m_impl.SetValue(topic, value, NT_EVENT_VALUE_REMOTE,
-                        value == topic->lastValue, false, nullptr)) {
-      topic->lastValueNetwork = value;
-      topic->lastValueFromNetwork = true;
+    if (m_impl.SetValue(topic, value, NT_EVENT_VALUE_REMOTE, false, nullptr)) {
+      if (topic->IsCached()) {
+        topic->lastValueNetwork = value;
+        topic->lastValueFromNetwork = true;
+      }
     }
   }
 }
@@ -1448,7 +1511,7 @@
   // start logging any matching topics
   auto now = nt::Now();
   for (auto&& topic : m_impl.m_topics) {
-    if (!wpi::starts_with(topic->name, prefix) ||
+    if (!PrefixMatch(topic->name, prefix, topic->special) ||
         topic->type == NT_UNASSIGNED || topic->typeStr.empty()) {
       continue;
     }
diff --git a/ntcore/src/main/native/cpp/LocalStorage.h b/ntcore/src/main/native/cpp/LocalStorage.h
index af2b4de..4283274 100644
--- a/ntcore/src/main/native/cpp/LocalStorage.h
+++ b/ntcore/src/main/native/cpp/LocalStorage.h
@@ -136,6 +136,22 @@
     }
   }
 
+  void SetTopicCached(NT_Topic topicHandle, bool value) {
+    std::scoped_lock lock{m_mutex};
+    if (auto topic = m_impl.m_topics.Get(topicHandle)) {
+      m_impl.SetCached(topic, value);
+    }
+  }
+
+  bool GetTopicCached(NT_Topic topicHandle) {
+    std::scoped_lock lock{m_mutex};
+    if (auto topic = m_impl.m_topics.Get(topicHandle)) {
+      return (topic->flags & NT_UNCACHED) == 0;
+    } else {
+      return false;
+    }
+  }
+
   bool GetTopicExists(NT_Handle handle) {
     std::scoped_lock lock{m_mutex};
     TopicData* topic = m_impl.GetTopic(handle);
@@ -361,6 +377,8 @@
 
     bool Exists() const { return onNetwork || !localPublishers.empty(); }
 
+    bool IsCached() const { return (flags & NT_UNCACHED) == 0; }
+
     TopicInfo GetTopicInfo() const;
 
     // invariants
@@ -565,14 +583,15 @@
     void CheckReset(TopicData* topic);
 
     bool SetValue(TopicData* topic, const Value& value, unsigned int eventFlags,
-                  bool isDuplicate, bool suppressIfDuplicate,
-                  const PublisherData* publisher);
-    void NotifyValue(TopicData* topic, unsigned int eventFlags,
-                     bool isDuplicate, const PublisherData* publisher);
+                  bool suppressIfDuplicate, const PublisherData* publisher);
+    void NotifyValue(TopicData* topic, const Value& value,
+                     unsigned int eventFlags, bool isDuplicate,
+                     const PublisherData* publisher);
 
     void SetFlags(TopicData* topic, unsigned int flags);
     void SetPersistent(TopicData* topic, bool value);
     void SetRetained(TopicData* topic, bool value);
+    void SetCached(TopicData* topic, bool value);
     void SetProperties(TopicData* topic, const wpi::json& update,
                        bool sendNetwork);
     void PropertiesUpdated(TopicData* topic, const wpi::json& update,
diff --git a/ntcore/src/main/native/cpp/NetworkClient.cpp b/ntcore/src/main/native/cpp/NetworkClient.cpp
index 7634be9..149dcb6 100644
--- a/ntcore/src/main/native/cpp/NetworkClient.cpp
+++ b/ntcore/src/main/native/cpp/NetworkClient.cpp
@@ -206,7 +206,9 @@
   auto clientImpl = std::make_shared<net3::ClientImpl3>(
       m_loop.Now().count(), m_inst, *wire, m_logger, [this](uint32_t repeatMs) {
         DEBUG4("Setting periodic timer to {}", repeatMs);
-        if (m_sendOutgoingTimer) {
+        if (m_sendOutgoingTimer &&
+            (!m_sendOutgoingTimer->IsActive() ||
+             uv::Timer::Time{repeatMs} != m_sendOutgoingTimer->GetRepeat())) {
           m_sendOutgoingTimer->Start(uv::Timer::Time{repeatMs},
                                      uv::Timer::Time{repeatMs});
         }
@@ -406,7 +408,9 @@
       m_loop.Now().count(), m_inst, *m_wire, m_logger, m_timeSyncUpdated,
       [this](uint32_t repeatMs) {
         DEBUG4("Setting periodic timer to {}", repeatMs);
-        if (m_sendOutgoingTimer) {
+        if (m_sendOutgoingTimer &&
+            (!m_sendOutgoingTimer->IsActive() ||
+             uv::Timer::Time{repeatMs} != m_sendOutgoingTimer->GetRepeat())) {
           m_sendOutgoingTimer->Start(uv::Timer::Time{repeatMs},
                                      uv::Timer::Time{repeatMs});
         }
diff --git a/ntcore/src/main/native/cpp/NetworkServer.cpp b/ntcore/src/main/native/cpp/NetworkServer.cpp
index 9062c7f..5b5e78d 100644
--- a/ntcore/src/main/native/cpp/NetworkServer.cpp
+++ b/ntcore/src/main/native/cpp/NetworkServer.cpp
@@ -112,7 +112,8 @@
   DEBUG4("Setting periodic timer to {}", repeatMs);
   if (repeatMs == UINT32_MAX) {
     m_outgoingTimer->Stop();
-  } else {
+  } else if (!m_outgoingTimer->IsActive() ||
+             uv::Timer::Time{repeatMs} != m_outgoingTimer->GetRepeat()) {
     m_outgoingTimer->Start(uv::Timer::Time{repeatMs},
                            uv::Timer::Time{repeatMs});
   }
diff --git a/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp
index 14b5df1..fdec2b9 100644
--- a/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp
+++ b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp
@@ -673,6 +673,30 @@
 
 /*
  * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setTopicCached
+ * Signature: (IZ)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setTopicCached
+  (JNIEnv*, jclass, jint topic, jboolean value)
+{
+  nt::SetTopicCached(topic, value);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getTopicCached
+ * Signature: (I)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicCached
+  (JNIEnv*, jclass, jint topic)
+{
+  return nt::GetTopicCached(topic);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
  * Method:    getTopicTypeString
  * Signature: (I)Ljava/lang/String;
  */
diff --git a/ntcore/src/main/native/cpp/net/ClientImpl.cpp b/ntcore/src/main/native/cpp/net/ClientImpl.cpp
index 309b01a..96a6be7 100644
--- a/ntcore/src/main/native/cpp/net/ClientImpl.cpp
+++ b/ntcore/src/main/native/cpp/net/ClientImpl.cpp
@@ -20,7 +20,6 @@
 #include "NetworkInterface.h"
 #include "WireConnection.h"
 #include "WireEncoder.h"
-#include "net/NetworkOutgoingQueue.h"
 #include "networktables/NetworkTableValue.h"
 
 using namespace nt;
@@ -68,26 +67,28 @@
     DEBUG4("BinaryMessage({})", id);
 
     // handle RTT ping response (only use first one)
-    if (!m_haveTimeOffset && id == -1) {
-      if (!value.IsInteger()) {
-        WARN("RTT ping response with non-integer type {}",
-             static_cast<int>(value.type()));
-        continue;
-      }
-      DEBUG4("RTT ping response time {} value {}", value.time(),
-             value.GetInteger());
-      if (m_wire.GetVersion() < 0x0401) {
-        m_pongTimeMs = curTimeMs;
-      }
-      int64_t now = wpi::Now();
-      int64_t rtt2 = (now - value.GetInteger()) / 2;
-      if (rtt2 < m_rtt2Us) {
-        m_rtt2Us = rtt2;
-        int64_t serverTimeOffsetUs = value.server_time() + rtt2 - now;
-        DEBUG3("Time offset: {}", serverTimeOffsetUs);
-        m_outgoing.SetTimeOffset(serverTimeOffsetUs);
-        m_haveTimeOffset = true;
-        m_timeSyncUpdated(serverTimeOffsetUs, m_rtt2Us, true);
+    if (id == -1) {
+      if (!m_haveTimeOffset) {
+        if (!value.IsInteger()) {
+          WARN("RTT ping response with non-integer type {}",
+               static_cast<int>(value.type()));
+          continue;
+        }
+        DEBUG4("RTT ping response time {} value {}", value.time(),
+               value.GetInteger());
+        if (m_wire.GetVersion() < 0x0401) {
+          m_pongTimeMs = curTimeMs;
+        }
+        int64_t now = wpi::Now();
+        int64_t rtt2 = (now - value.GetInteger()) / 2;
+        if (rtt2 < m_rtt2Us) {
+          m_rtt2Us = rtt2;
+          int64_t serverTimeOffsetUs = value.server_time() + rtt2 - now;
+          DEBUG3("Time offset: {}", serverTimeOffsetUs);
+          m_outgoing.SetTimeOffset(serverTimeOffsetUs);
+          m_haveTimeOffset = true;
+          m_timeSyncUpdated(serverTimeOffsetUs, m_rtt2Us, true);
+        }
       }
       continue;
     }
diff --git a/ntcore/src/main/native/cpp/net/ServerImpl.cpp b/ntcore/src/main/native/cpp/net/ServerImpl.cpp
index 1119809..8dc5291 100644
--- a/ntcore/src/main/native/cpp/net/ServerImpl.cpp
+++ b/ntcore/src/main/native/cpp/net/ServerImpl.cpp
@@ -917,6 +917,7 @@
   auto typeStr = TypeToString(value.type());
   wpi::json properties = wpi::json::object();
   properties["retained"] = true;  // treat all NT3 published topics as retained
+  properties["cached"] = true;    // treat all NT3 published topics as cached
   if ((flags & NT_PERSISTENT) != 0) {
     properties["persistent"] = true;
   }
@@ -1095,6 +1096,7 @@
 void ServerImpl::TopicData::RefreshProperties() {
   persistent = false;
   retained = false;
+  cached = true;
 
   auto persistentIt = properties.find("persistent");
   if (persistentIt != properties.end()) {
@@ -1109,6 +1111,23 @@
       retained = *val;
     }
   }
+
+  auto cachedIt = properties.find("cached");
+  if (cachedIt != properties.end()) {
+    if (auto val = cachedIt->get_ptr<bool*>()) {
+      cached = *val;
+    }
+  }
+
+  if (!cached) {
+    lastValue = {};
+    lastValueClient = nullptr;
+  }
+
+  if (!cached && persistent) {
+    WARN("topic {}: disabling cached property disables persistent storage",
+         name);
+  }
 }
 
 bool ServerImpl::TopicData::SetFlags(unsigned int flags_) {
@@ -1122,6 +1141,30 @@
     persistent = false;
     properties.erase("persistent");
   }
+  if ((flags_ & NT_RETAINED) != 0) {
+    updated |= !retained;
+    retained = true;
+    properties["retained"] = true;
+  } else {
+    updated |= retained;
+    retained = false;
+    properties.erase("retained");
+  }
+  if ((flags_ & NT_UNCACHED) != 0) {
+    updated |= cached;
+    cached = false;
+    properties["cached"] = false;
+    lastValue = {};
+    lastValueClient = nullptr;
+  } else {
+    updated |= !cached;
+    cached = true;
+    properties.erase("cached");
+  }
+  if (!cached && persistent) {
+    WARN("topic {}: disabling cached property disables persistent storage",
+         name);
+  }
   return updated;
 }
 
@@ -1642,7 +1685,7 @@
   } else {
     // new topic
     unsigned int id = m_topics.emplace_back(
-        std::make_unique<TopicData>(name, typeStr, properties));
+        std::make_unique<TopicData>(m_logger, name, typeStr, properties));
     topic = m_topics[id].get();
     topic->id = id;
     topic->special = special;
@@ -1663,8 +1706,14 @@
       }
 
       auto& tcd = topic->clients[aClient.get()];
+      bool added = false;
       for (auto subscriber : subscribers) {
-        tcd.AddSubscriber(subscriber);
+        if (tcd.AddSubscriber(subscriber)) {
+          added = true;
+        }
+      }
+      if (added) {
+        aClient->UpdatePeriod(tcd, topic);
       }
 
       if (aClient.get() == client) {
@@ -1707,6 +1756,7 @@
   // unannounce to all subscribers
   for (auto&& tcd : topic->clients) {
     if (!tcd.second.subscribers.empty()) {
+      tcd.first->UpdatePeriod(tcd.second, topic);
       tcd.first->SendUnannounce(topic);
     }
   }
@@ -1751,8 +1801,9 @@
 void ServerImpl::SetValue(ClientData* client, TopicData* topic,
                           const Value& value) {
   // update retained value if from same client or timestamp newer
-  if (!topic->lastValue || topic->lastValueClient == client ||
-      topic->lastValue.time() == 0 || value.time() >= topic->lastValue.time()) {
+  if (topic->cached && (!topic->lastValue || topic->lastValueClient == client ||
+                        topic->lastValue.time() == 0 ||
+                        value.time() >= topic->lastValue.time())) {
     DEBUG4("updating '{}' last value (time was {} is {})", topic->name,
            topic->lastValue.time(), value.time());
     topic->lastValue = value;
diff --git a/ntcore/src/main/native/cpp/net/ServerImpl.h b/ntcore/src/main/native/cpp/net/ServerImpl.h
index 3ea5a82..d84a06c 100644
--- a/ntcore/src/main/native/cpp/net/ServerImpl.h
+++ b/ntcore/src/main/native/cpp/net/ServerImpl.h
@@ -98,11 +98,15 @@
   struct SubscriberData;
 
   struct TopicData {
-    TopicData(std::string_view name, std::string_view typeStr)
-        : name{name}, typeStr{typeStr} {}
-    TopicData(std::string_view name, std::string_view typeStr,
-              wpi::json properties)
-        : name{name}, typeStr{typeStr}, properties(std::move(properties)) {
+    TopicData(wpi::Logger& logger, std::string_view name,
+              std::string_view typeStr)
+        : m_logger{logger}, name{name}, typeStr{typeStr} {}
+    TopicData(wpi::Logger& logger, std::string_view name,
+              std::string_view typeStr, wpi::json properties)
+        : m_logger{logger},
+          name{name},
+          typeStr{typeStr},
+          properties(std::move(properties)) {
       RefreshProperties();
     }
 
@@ -117,6 +121,7 @@
 
     NT_Handle GetIdHandle() const { return Handle(0, id, Handle::kTopic); }
 
+    wpi::Logger& m_logger;  // Must be m_logger for WARN macro to work
     std::string name;
     unsigned int id;
     Value lastValue;
@@ -126,6 +131,7 @@
     unsigned int publisherCount{0};
     bool persistent{false};
     bool retained{false};
+    bool cached{true};
     bool special{false};
     NT_Topic localHandle{0};
 
@@ -148,10 +154,12 @@
 
       bool AddSubscriber(SubscriberData* sub) {
         bool added = subscribers.insert(sub).second;
-        if (!sub->options.topicsOnly && sendMode == ValueSendMode::kDisabled) {
-          sendMode = ValueSendMode::kNormal;
-        } else if (sub->options.sendAll) {
-          sendMode = ValueSendMode::kAll;
+        if (!sub->options.topicsOnly) {
+          if (sub->options.sendAll) {
+            sendMode = ValueSendMode::kAll;
+          } else if (sendMode == ValueSendMode::kDisabled) {
+            sendMode = ValueSendMode::kNormal;
+          }
         }
         return added;
       }
@@ -200,9 +208,10 @@
     std::string_view GetName() const { return m_name; }
     int GetId() const { return m_id; }
 
-   protected:
-    virtual void UpdatePeriodic(TopicData* topic) {}
+    virtual void UpdatePeriod(TopicData::TopicClientData& tcd,
+                              TopicData* topic) {}
 
+   protected:
     std::string m_name;
     std::string m_connInfo;
     bool m_local;  // local to machine
@@ -245,9 +254,6 @@
 
     void ClientSetValue(int64_t pubuid, const Value& value);
 
-    virtual void UpdatePeriod(TopicData::TopicClientData& tcd,
-                              TopicData* topic) {}
-
     wpi::DenseMap<TopicData*, bool> m_announceSent;
   };
 
diff --git a/ntcore/src/main/native/cpp/net/WebSocketConnection.cpp b/ntcore/src/main/native/cpp/net/WebSocketConnection.cpp
index dcfbabe..74b5ecf 100644
--- a/ntcore/src/main/native/cpp/net/WebSocketConnection.cpp
+++ b/ntcore/src/main/native/cpp/net/WebSocketConnection.cpp
@@ -54,8 +54,14 @@
     // flush_nonempty() case
     m_conn.m_bufs.back().len = len;
     if (!m_disableAlloc) {
+#ifdef NT_ENABLE_WS_FRAG
       m_conn.m_frames.back().opcode &= ~wpi::WebSocket::kFlagFin;
       m_conn.StartFrame(wpi::WebSocket::Frame::kFragment);
+#else
+      m_conn.m_bufs.emplace_back(m_conn.AllocBuf());
+      m_conn.m_bufs.back().len = 0;
+      ++m_conn.m_frames.back().end;
+#endif
       SetBuffer(m_conn.m_bufs.back().base, kAllocSize);
     }
     return;
@@ -76,9 +82,15 @@
       len -= amt;
     }
     if (buf.len >= kAllocSize && (len > 0 || !m_disableAlloc)) {
+#ifdef NT_ENABLE_WS_FRAG
       // fragment the current frame and start a new one
       m_conn.m_frames.back().opcode &= ~wpi::WebSocket::kFlagFin;
       m_conn.StartFrame(wpi::WebSocket::Frame::kFragment);
+#else
+      m_conn.m_bufs.emplace_back(m_conn.AllocBuf());
+      m_conn.m_bufs.back().len = 0;
+      ++m_conn.m_frames.back().end;
+#endif
       updateBuffer = true;
     }
   }
diff --git a/ntcore/src/main/native/cpp/ntcore_c.cpp b/ntcore/src/main/native/cpp/ntcore_c.cpp
index b432664..c4322b3 100644
--- a/ntcore/src/main/native/cpp/ntcore_c.cpp
+++ b/ntcore/src/main/native/cpp/ntcore_c.cpp
@@ -295,6 +295,14 @@
   return nt::GetTopicRetained(topic);
 }
 
+void NT_SetTopicCached(NT_Topic topic, NT_Bool value) {
+  nt::SetTopicCached(topic, value);
+}
+
+NT_Bool NT_GetTopicCached(NT_Topic topic) {
+  return nt::GetTopicCached(topic);
+}
+
 NT_Bool NT_GetTopicExists(NT_Handle handle) {
   return nt::GetTopicExists(handle);
 }
diff --git a/ntcore/src/main/native/cpp/ntcore_cpp.cpp b/ntcore/src/main/native/cpp/ntcore_cpp.cpp
index 4b48f05..b27c4f5 100644
--- a/ntcore/src/main/native/cpp/ntcore_cpp.cpp
+++ b/ntcore/src/main/native/cpp/ntcore_cpp.cpp
@@ -263,6 +263,22 @@
   }
 }
 
+void SetTopicCached(NT_Topic topic, bool value) {
+  if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+    ii->localStorage.SetTopicCached(topic, value);
+  } else {
+    return;
+  }
+}
+
+bool GetTopicCached(NT_Topic topic) {
+  if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+    return ii->localStorage.GetTopicCached(topic);
+  } else {
+    return {};
+  }
+}
+
 bool GetTopicExists(NT_Handle handle) {
   if (auto ii = InstanceImpl::GetHandle(handle)) {
     return ii->localStorage.GetTopicExists(handle);
diff --git a/ntcore/src/main/native/cpp/ntcore_meta.cpp b/ntcore/src/main/native/cpp/ntcore_meta.cpp
index 05cb4b7..3b947a0 100644
--- a/ntcore/src/main/native/cpp/ntcore_meta.cpp
+++ b/ntcore/src/main/native/cpp/ntcore_meta.cpp
@@ -39,7 +39,7 @@
     std::span<const uint8_t> data) {
   mpack_reader_t r;
   mpack_reader_init_data(&r, data);
-  uint32_t numPub = mpack_expect_array_max(&r, 1000);
+  uint32_t numPub = mpack_expect_array_max(&r, 10000);
   std::vector<ClientPublisher> publishers;
   publishers.reserve(numPub);
   for (uint32_t i = 0; i < numPub; ++i) {
@@ -71,7 +71,7 @@
     std::span<const uint8_t> data) {
   mpack_reader_t r;
   mpack_reader_init_data(&r, data);
-  uint32_t numSub = mpack_expect_array_max(&r, 1000);
+  uint32_t numSub = mpack_expect_array_max(&r, 10000);
   std::vector<ClientSubscriber> subscribers;
   subscribers.reserve(numSub);
   for (uint32_t i = 0; i < numSub; ++i) {
diff --git a/ntcore/src/main/native/include/networktables/NetworkTable.h b/ntcore/src/main/native/include/networktables/NetworkTable.h
index e03ea9e..1d93c8c 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTable.h
+++ b/ntcore/src/main/native/include/networktables/NetworkTable.h
@@ -37,9 +37,11 @@
 class RawTopic;
 class StringArrayTopic;
 class StringTopic;
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructArrayTopic;
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructTopic;
 class Topic;
 
@@ -246,9 +248,10 @@
    * @param name topic name
    * @return Topic
    */
-  template <wpi::StructSerializable T>
-  StructTopic<T> GetStructTopic(std::string_view name) const {
-    return StructTopic<T>{GetTopic(name)};
+  template <typename T, typename... I>
+    requires wpi::StructSerializable<T, I...>
+  StructTopic<T, I...> GetStructTopic(std::string_view name) const {
+    return StructTopic<T, I...>{GetTopic(name)};
   }
 
   /**
@@ -257,9 +260,10 @@
    * @param name topic name
    * @return Topic
    */
-  template <wpi::StructSerializable T>
-  StructArrayTopic<T> GetStructArrayTopic(std::string_view name) const {
-    return StructArrayTopic<T>{GetTopic(name)};
+  template <typename T, typename... I>
+    requires wpi::StructSerializable<T, I...>
+  StructArrayTopic<T, I...> GetStructArrayTopic(std::string_view name) const {
+    return StructArrayTopic<T, I...>{GetTopic(name)};
   }
 
   /**
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableInstance.h b/ntcore/src/main/native/include/networktables/NetworkTableInstance.h
index 06e2cd6..cf8f8e1 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableInstance.h
+++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.h
@@ -37,9 +37,11 @@
 class RawTopic;
 class StringArrayTopic;
 class StringTopic;
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructArrayTopic;
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructTopic;
 class Subscriber;
 class Topic;
@@ -260,19 +262,24 @@
    * Gets a raw struct serialized value topic.
    *
    * @param name topic name
+   * @param info optional struct type info
    * @return Topic
    */
-  template <wpi::StructSerializable T>
-  StructTopic<T> GetStructTopic(std::string_view name) const;
+  template <typename T, typename... I>
+    requires wpi::StructSerializable<T, I...>
+  StructTopic<T, I...> GetStructTopic(std::string_view name, I... info) const;
 
   /**
    * Gets a raw struct serialized array topic.
    *
    * @param name topic name
+   * @param info optional struct type info
    * @return Topic
    */
-  template <wpi::StructSerializable T>
-  StructArrayTopic<T> GetStructArrayTopic(std::string_view name) const;
+  template <typename T, typename... I>
+    requires wpi::StructSerializable<T, I...>
+  StructArrayTopic<T, I...> GetStructArrayTopic(std::string_view name,
+                                                I... info) const;
 
   /**
    * Get Published Topics.
@@ -818,10 +825,12 @@
    * Registers a struct schema. Duplicate calls to this function with the same
    * name are silently ignored.
    *
-   * @param T struct serializable type
+   * @tparam T struct serializable type
+   * @param info optional struct type info
    */
-  template <wpi::StructSerializable T>
-  void AddStructSchema();
+  template <typename T, typename... I>
+    requires wpi::StructSerializable<T, I...>
+  void AddStructSchema(const I&... info);
 
   /**
    * Equality operator.  Returns true if both instances refer to the same
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc
index fdd517e..e583e59 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc
+++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc
@@ -44,16 +44,18 @@
   return ProtobufTopic<T>{GetTopic(name)};
 }
 
-template <wpi::StructSerializable T>
-inline StructTopic<T> NetworkTableInstance::GetStructTopic(
-    std::string_view name) const {
-  return StructTopic<T>{GetTopic(name)};
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
+inline StructTopic<T, I...> NetworkTableInstance::GetStructTopic(
+    std::string_view name, I... info) const {
+  return StructTopic<T, I...>{GetTopic(name), std::move(info)...};
 }
 
-template <wpi::StructSerializable T>
-inline StructArrayTopic<T> NetworkTableInstance::GetStructArrayTopic(
-    std::string_view name) const {
-  return StructArrayTopic<T>{GetTopic(name)};
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
+inline StructArrayTopic<T, I...> NetworkTableInstance::GetStructArrayTopic(
+    std::string_view name, I... info) const {
+  return StructArrayTopic<T, I...>{GetTopic(name), std::move(info)...};
 }
 
 inline std::vector<Topic> NetworkTableInstance::GetTopics() {
@@ -257,6 +259,12 @@
   ::nt::AddSchema(m_handle, name, type, schema);
 }
 
+// Suppress unused-lambda-capture warning on AddSchema() call
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-lambda-capture"
+#endif
+
 template <wpi::ProtobufSerializable T>
 void NetworkTableInstance::AddProtobufSchema(wpi::ProtobufMessage<T>& msg) {
   msg.ForEachProtobufDescriptor(
@@ -266,11 +274,18 @@
       });
 }
 
-template <wpi::StructSerializable T>
-void NetworkTableInstance::AddStructSchema() {
-  wpi::ForEachStructSchema<T>([this](auto typeString, auto schema) {
-    AddSchema(typeString, "structschema", schema);
-  });
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
+void NetworkTableInstance::AddStructSchema(const I&... info) {
+  wpi::ForEachStructSchema<T>(
+      [this](auto typeString, auto schema) {
+        AddSchema(typeString, "structschema", schema);
+      },
+      info...);
 }
 
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
 }  // namespace nt
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableType.h b/ntcore/src/main/native/include/networktables/NetworkTableType.h
index 4b60454..3b3e086 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableType.h
+++ b/ntcore/src/main/native/include/networktables/NetworkTableType.h
@@ -13,17 +13,29 @@
  * @ingroup ntcore_cpp_api
  */
 enum class NetworkTableType {
+  /// Unassigned data type.
   kUnassigned = NT_UNASSIGNED,
+  /// Boolean data type.
   kBoolean = NT_BOOLEAN,
+  /// Double precision floating-point data type.
   kDouble = NT_DOUBLE,
+  /// String data type.
   kString = NT_STRING,
+  /// Raw data type.
   kRaw = NT_RAW,
+  /// Boolean array data type.
   kBooleanArray = NT_BOOLEAN_ARRAY,
+  /// Double precision floating-point array data type.
   kDoubleArray = NT_DOUBLE_ARRAY,
+  /// String array data type.
   kStringArray = NT_STRING_ARRAY,
+  /// Integer data type.
   kInteger = NT_INTEGER,
+  /// Single precision floating-point data type.
   kFloat = NT_FLOAT,
+  /// Integer array data type.
   kIntegerArray = NT_INTEGER_ARRAY,
+  /// Single precision floating-point array data type.
   kFloatArray = NT_FLOAT_ARRAY
 };
 
diff --git a/ntcore/src/main/native/include/networktables/ProtobufTopic.h b/ntcore/src/main/native/include/networktables/ProtobufTopic.h
index 4c30bf7..7a56759 100644
--- a/ntcore/src/main/native/include/networktables/ProtobufTopic.h
+++ b/ntcore/src/main/native/include/networktables/ProtobufTopic.h
@@ -14,6 +14,7 @@
 #include <vector>
 
 #include <wpi/SmallVector.h>
+#include <wpi/json_fwd.h>
 #include <wpi/mutex.h>
 #include <wpi/protobuf/Protobuf.h>
 
@@ -21,10 +22,6 @@
 #include "networktables/Topic.h"
 #include "ntcore_cpp.h"
 
-namespace wpi {
-class json;
-}  // namespace wpi
-
 namespace nt {
 
 template <wpi::ProtobufSerializable T>
@@ -63,12 +60,12 @@
   ProtobufSubscriber(ProtobufSubscriber&& rhs)
       : Subscriber{std::move(rhs)},
         m_msg{std::move(rhs.m_msg)},
-        m_defaultValue{std::move(rhs.defaultValue)} {}
+        m_defaultValue{std::move(rhs.m_defaultValue)} {}
 
   ProtobufSubscriber& operator=(ProtobufSubscriber&& rhs) {
     Subscriber::operator=(std::move(rhs));
     m_msg = std::move(rhs.m_msg);
-    m_defaultValue = std::move(rhs.defaultValue);
+    m_defaultValue = std::move(rhs.m_defaultValue);
     return *this;
   }
 
@@ -175,8 +172,8 @@
   }
 
  private:
-  wpi::mutex m_mutex;
-  wpi::ProtobufMessage<T> m_msg;
+  mutable wpi::mutex m_mutex;
+  mutable wpi::ProtobufMessage<T> m_msg;
   ValueType m_defaultValue;
 };
 
@@ -215,10 +212,9 @@
   ProtobufPublisher& operator=(ProtobufPublisher&& rhs) {
     Publisher::operator=(std::move(rhs));
     m_msg = std::move(rhs.m_msg);
-    m_schemaPublished.clear();
-    if (rhs.m_schemaPublished.test()) {
-      m_schemaPublished.test_and_set();
-    }
+    m_schemaPublished.store(
+        rhs.m_schemaPublished.load(std::memory_order_relaxed),
+        std::memory_order_relaxed);
     return *this;
   }
 
@@ -232,7 +228,7 @@
     wpi::SmallVector<uint8_t, 128> buf;
     {
       std::scoped_lock lock{m_mutex};
-      if (!m_schemaPublished.test_and_set()) {
+      if (!m_schemaPublished.exchange(true, std::memory_order_relaxed)) {
         GetTopic().GetInstance().template AddProtobufSchema<T>(m_msg);
       }
       m_msg.Pack(buf, value);
@@ -251,7 +247,7 @@
     wpi::SmallVector<uint8_t, 128> buf;
     {
       std::scoped_lock lock{m_mutex};
-      if (!m_schemaPublished.test_and_set()) {
+      if (!m_schemaPublished.exchange(true, std::memory_order_relaxed)) {
         GetTopic().GetInstance().template AddProtobufSchema<T>(m_msg);
       }
       m_msg.Pack(buf, value);
@@ -271,7 +267,7 @@
  private:
   wpi::mutex m_mutex;
   wpi::ProtobufMessage<T> m_msg;
-  std::atomic_flag m_schemaPublished = ATOMIC_FLAG_INIT;
+  std::atomic_bool m_schemaPublished{false};
 };
 
 /**
@@ -303,7 +299,7 @@
    */
   ProtobufEntry(NT_Entry handle, wpi::ProtobufMessage<T> msg, T defaultValue)
       : ProtobufSubscriber<T>{handle, std::move(msg), std::move(defaultValue)},
-        ProtobufPublisher<T>{handle, {}} {}
+        ProtobufPublisher<T>{handle, wpi::ProtobufMessage<T>{}} {}
 
   /**
    * Determines if the native handle is valid.
diff --git a/ntcore/src/main/native/include/networktables/StructArrayTopic.h b/ntcore/src/main/native/include/networktables/StructArrayTopic.h
index 91f4721..667570b 100644
--- a/ntcore/src/main/native/include/networktables/StructArrayTopic.h
+++ b/ntcore/src/main/native/include/networktables/StructArrayTopic.h
@@ -7,13 +7,16 @@
 #include <stdint.h>
 
 #include <atomic>
+#include <functional>
+#include <memory>
 #include <ranges>
 #include <span>
-#include <string_view>
+#include <tuple>
 #include <utility>
 #include <vector>
 
 #include <wpi/SmallVector.h>
+#include <wpi/json_fwd.h>
 #include <wpi/mutex.h>
 #include <wpi/struct/Struct.h>
 
@@ -21,24 +24,22 @@
 #include "networktables/Topic.h"
 #include "ntcore_cpp.h"
 
-namespace wpi {
-class json;
-}  // namespace wpi
-
 namespace nt {
 
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructArrayTopic;
 
 /**
  * NetworkTables struct-encoded value array subscriber.
  */
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructArraySubscriber : public Subscriber {
-  using S = wpi::Struct<T>;
+  using S = wpi::Struct<T, I...>;
 
  public:
-  using TopicType = StructArrayTopic<T>;
+  using TopicType = StructArrayTopic<T, I...>;
   using ValueType = std::vector<T>;
   using ParamType = std::span<const T>;
   using TimestampedValueType = Timestamped<ValueType>;
@@ -51,15 +52,17 @@
    *
    * @param handle Native handle
    * @param defaultValue Default value
+   * @param info optional struct type info
    */
   template <typename U>
 #if __cpp_lib_ranges >= 201911L
     requires std::ranges::range<U> &&
                  std::convertible_to<std::ranges::range_value_t<U>, T>
 #endif
-  StructArraySubscriber(NT_Subscriber handle, U&& defaultValue)
+  StructArraySubscriber(NT_Subscriber handle, U&& defaultValue, I... info)
       : Subscriber{handle},
-        m_defaultValue{defaultValue.begin(), defaultValue.end()} {
+        m_defaultValue{defaultValue.begin(), defaultValue.end()},
+        m_info{std::move(info)...} {
   }
 
   /**
@@ -124,16 +127,21 @@
 #endif
   TimestampedValueType GetAtomic(U&& defaultValue) const {
     wpi::SmallVector<uint8_t, 128> buf;
+    size_t size = std::apply(S::GetSize, m_info);
     TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
-    if (view.value.size() == 0 || (view.value.size() % S::kSize) != 0) {
+    if (view.value.size() == 0 || (view.value.size() % size) != 0) {
       return {0, 0, std::forward<U>(defaultValue)};
     }
     TimestampedValueType rv{view.time, view.serverTime, {}};
-    rv.value.reserve(view.value.size() / S::kSize);
+    rv.value.reserve(view.value.size() / size);
     for (auto in = view.value.begin(), end = view.value.end(); in != end;
-         in += S::kSize) {
-      rv.value.emplace_back(
-          S::Unpack(std::span<const uint8_t, S::kSize>{in, in + S::kSize}));
+         in += size) {
+      std::apply(
+          [&](const I&... info) {
+            rv.value.emplace_back(S::Unpack(
+                std::span<const uint8_t>{std::to_address(in), size}, info...));
+          },
+          m_info);
     }
     return rv;
   }
@@ -148,16 +156,21 @@
    */
   TimestampedValueType GetAtomic(std::span<const T> defaultValue) const {
     wpi::SmallVector<uint8_t, 128> buf;
+    size_t size = std::apply(S::GetSize, m_info);
     TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
-    if (view.value.size() == 0 || (view.value.size() % S::kSize) != 0) {
+    if (view.value.size() == 0 || (view.value.size() % size) != 0) {
       return {0, 0, {defaultValue.begin(), defaultValue.end()}};
     }
     TimestampedValueType rv{view.time, view.serverTime, {}};
-    rv.value.reserve(view.value.size() / S::kSize);
+    rv.value.reserve(view.value.size() / size);
     for (auto in = view.value.begin(), end = view.value.end(); in != end;
-         in += S::kSize) {
-      rv.value.emplace_back(
-          S::Unpack(std::span<const uint8_t, S::kSize>{in, in + S::kSize}));
+         in += size) {
+      std::apply(
+          [&](const I&... info) {
+            rv.value.emplace_back(S::Unpack(
+                std::span<const uint8_t>{std::to_address(in), size}, info...));
+          },
+          m_info);
     }
     return rv;
   }
@@ -177,16 +190,22 @@
     auto raw = ::nt::ReadQueueRaw(m_subHandle);
     std::vector<TimestampedValueType> rv;
     rv.reserve(raw.size());
+    size_t size = std::apply(S::GetSize, m_info);
     for (auto&& r : raw) {
-      if (r.value.size() == 0 || (r.value.size() % S::kSize) != 0) {
+      if (r.value.size() == 0 || (r.value.size() % size) != 0) {
         continue;
       }
       std::vector<T> values;
-      values.reserve(r.value.size() / S::kSize);
+      values.reserve(r.value.size() / size);
       for (auto in = r.value.begin(), end = r.value.end(); in != end;
-           in += S::kSize) {
-        values.emplace_back(
-            S::Unpack(std::span<const uint8_t, S::kSize>{in, in + S::kSize}));
+           in += size) {
+        std::apply(
+            [&](const I&... info) {
+              values.emplace_back(
+                  S::Unpack(std::span<const uint8_t>{std::to_address(in), size},
+                            info...));
+            },
+            m_info);
       }
       rv.emplace_back(r.time, r.serverTime, std::move(values));
     }
@@ -199,22 +218,29 @@
    * @return Topic
    */
   TopicType GetTopic() const {
-    return StructArrayTopic<T>{::nt::GetTopicFromHandle(m_subHandle)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructArrayTopic<T, I...>{
+              ::nt::GetTopicFromHandle(m_subHandle), info...};
+        },
+        m_info);
   }
 
  private:
   ValueType m_defaultValue;
+  [[no_unique_address]] std::tuple<I...> m_info;
 };
 
 /**
  * NetworkTables struct-encoded value array publisher.
  */
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructArrayPublisher : public Publisher {
-  using S = wpi::Struct<T>;
+  using S = wpi::Struct<T, I...>;
 
  public:
-  using TopicType = StructArrayTopic<T>;
+  using TopicType = StructArrayTopic<T, I...>;
   using ValueType = std::vector<T>;
   using ParamType = std::span<const T>;
 
@@ -227,8 +253,10 @@
    * StructTopic::Publish() instead.
    *
    * @param handle Native handle
+   * @param info optional struct type info
    */
-  explicit StructArrayPublisher(NT_Publisher handle) : Publisher{handle} {}
+  explicit StructArrayPublisher(NT_Publisher handle, I... info)
+      : Publisher{handle}, m_info{std::move(info)...} {}
 
   StructArrayPublisher(const StructArrayPublisher&) = delete;
   StructArrayPublisher& operator=(const StructArrayPublisher&) = delete;
@@ -236,15 +264,17 @@
   StructArrayPublisher(StructArrayPublisher&& rhs)
       : Publisher{std::move(rhs)},
         m_buf{std::move(rhs.m_buf)},
-        m_schemaPublished{rhs.m_schemaPublished} {}
+        m_schemaPublished{
+            rhs.m_schemaPublished.load(std::memory_order_relaxed)},
+        m_info{std::move(rhs.m_info)} {}
 
   StructArrayPublisher& operator=(StructArrayPublisher&& rhs) {
     Publisher::operator=(std::move(rhs));
     m_buf = std::move(rhs.m_buf);
-    m_schemaPublished.clear();
-    if (rhs.m_schemaPublished.test()) {
-      m_schemaPublished.test_and_set();
-    }
+    m_schemaPublished.store(
+        rhs.m_schemaPublished.load(std::memory_order_relaxed),
+        std::memory_order_relaxed);
+    m_info = std::move(rhs.m_info);
     return *this;
   }
 
@@ -260,11 +290,17 @@
              std::convertible_to<std::ranges::range_value_t<U>, T>
 #endif
   void Set(U&& value, int64_t time = 0) {
-    if (!m_schemaPublished.test_and_set()) {
-      GetTopic().GetInstance().template AddStructSchema<T>();
-    }
-    m_buf.Write(std::forward<U>(value),
-                [&](auto bytes) { ::nt::SetRaw(m_pubHandle, bytes, time); });
+    std::apply(
+        [&](const I&... info) {
+          if (!m_schemaPublished.exchange(true, std::memory_order_relaxed)) {
+            GetTopic().GetInstance().template AddStructSchema<T>(info...);
+          }
+          m_buf.Write(
+              std::forward<U>(value),
+              [&](auto bytes) { ::nt::SetRaw(m_pubHandle, bytes, time); },
+              info...);
+        },
+        m_info);
   }
 
   /**
@@ -274,8 +310,14 @@
    * @param time timestamp; 0 indicates current NT time should be used
    */
   void Set(std::span<const T> value, int64_t time = 0) {
-    m_buf.Write(value,
-                [&](auto bytes) { ::nt::SetRaw(m_pubHandle, bytes, time); });
+    std::apply(
+        [&](const I&... info) {
+          m_buf.Write(
+              value,
+              [&](auto bytes) { ::nt::SetRaw(m_pubHandle, bytes, time); },
+              info...);
+        },
+        m_info);
   }
 
   /**
@@ -291,11 +333,17 @@
              std::convertible_to<std::ranges::range_value_t<U>, T>
 #endif
   void SetDefault(U&& value) {
-    if (!m_schemaPublished.test_and_set()) {
-      GetTopic().GetInstance().template AddStructSchema<T>();
-    }
-    m_buf.Write(std::forward<U>(value),
-                [&](auto bytes) { ::nt::SetDefaultRaw(m_pubHandle, bytes); });
+    std::apply(
+        [&](const I&... info) {
+          if (!m_schemaPublished.exchange(true, std::memory_order_relaxed)) {
+            GetTopic().GetInstance().template AddStructSchema<T>(info...);
+          }
+          m_buf.Write(
+              std::forward<U>(value),
+              [&](auto bytes) { ::nt::SetDefaultRaw(m_pubHandle, bytes); },
+              info...);
+        },
+        m_info);
   }
 
   /**
@@ -306,8 +354,14 @@
    * @param value value
    */
   void SetDefault(std::span<const T> value) {
-    m_buf.Write(value,
-                [&](auto bytes) { ::nt::SetDefaultRaw(m_pubHandle, bytes); });
+    std::apply(
+        [&](const I&... info) {
+          m_buf.Write(
+              value,
+              [&](auto bytes) { ::nt::SetDefaultRaw(m_pubHandle, bytes); },
+              info...);
+        },
+        m_info);
   }
 
   /**
@@ -316,12 +370,18 @@
    * @return Topic
    */
   TopicType GetTopic() const {
-    return StructArrayTopic<T>{::nt::GetTopicFromHandle(m_pubHandle)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructArrayTopic<T, I...>{
+              ::nt::GetTopicFromHandle(m_pubHandle), info...};
+        },
+        m_info);
   }
 
  private:
-  wpi::StructArrayBuffer<T> m_buf;
-  std::atomic_flag m_schemaPublished = ATOMIC_FLAG_INIT;
+  wpi::StructArrayBuffer<T, I...> m_buf;
+  std::atomic_bool m_schemaPublished{false};
+  [[no_unique_address]] std::tuple<I...> m_info;
 };
 
 /**
@@ -329,13 +389,14 @@
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-template <wpi::StructSerializable T>
-class StructArrayEntry final : public StructArraySubscriber<T>,
-                               public StructArrayPublisher<T> {
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
+class StructArrayEntry final : public StructArraySubscriber<T, I...>,
+                               public StructArrayPublisher<T, I...> {
  public:
-  using SubscriberType = StructArraySubscriber<T>;
-  using PublisherType = StructArrayPublisher<T>;
-  using TopicType = StructArrayTopic<T>;
+  using SubscriberType = StructArraySubscriber<T, I...>;
+  using PublisherType = StructArrayPublisher<T, I...>;
+  using TopicType = StructArrayTopic<T, I...>;
   using ValueType = std::vector<T>;
   using ParamType = std::span<const T>;
 
@@ -349,15 +410,16 @@
    *
    * @param handle Native handle
    * @param defaultValue Default value
+   * @param info optional struct type info
    */
   template <typename U>
 #if __cpp_lib_ranges >= 201911L
     requires std::ranges::range<U> &&
                  std::convertible_to<std::ranges::range_value_t<U>, T>
 #endif
-  StructArrayEntry(NT_Entry handle, U&& defaultValue)
-      : StructArraySubscriber<T>{handle, defaultValue},
-        StructArrayPublisher<T>{handle} {
+  StructArrayEntry(NT_Entry handle, U&& defaultValue, const I&... info)
+      : StructArraySubscriber<T, I...>{handle, defaultValue, info...},
+        StructArrayPublisher<T, I...>{handle, info...} {
   }
 
   /**
@@ -380,7 +442,7 @@
    * @return Topic
    */
   TopicType GetTopic() const {
-    return StructArrayTopic<T>{::nt::GetTopicFromHandle(this->m_subHandle)};
+    return StructArraySubscriber<T, I...>::GetTopic();
   }
 
   /**
@@ -392,12 +454,13 @@
 /**
  * NetworkTables struct-encoded value array topic.
  */
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructArrayTopic final : public Topic {
  public:
-  using SubscriberType = StructArraySubscriber<T>;
-  using PublisherType = StructArrayPublisher<T>;
-  using EntryType = StructArrayEntry<T>;
+  using SubscriberType = StructArraySubscriber<T, I...>;
+  using PublisherType = StructArrayPublisher<T, I...>;
+  using EntryType = StructArrayEntry<T, I...>;
   using ValueType = std::vector<T>;
   using ParamType = std::span<const T>;
   using TimestampedValueType = Timestamped<ValueType>;
@@ -409,15 +472,19 @@
    * NetworkTableInstance::GetStructTopic() instead.
    *
    * @param handle Native handle
+   * @param info optional struct type info
    */
-  explicit StructArrayTopic(NT_Topic handle) : Topic{handle} {}
+  explicit StructArrayTopic(NT_Topic handle, I... info)
+      : Topic{handle}, m_info{std::move(info)...} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
+   * @param info optional struct type info
    */
-  explicit StructArrayTopic(Topic topic) : Topic{topic} {}
+  explicit StructArrayTopic(Topic topic, I... info)
+      : Topic{topic}, m_info{std::move(info)...} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -442,11 +509,17 @@
   [[nodiscard]]
   SubscriberType Subscribe(
       U&& defaultValue, const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructArraySubscriber<T>{
-        ::nt::Subscribe(
-            m_handle, NT_RAW,
-            wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(), options),
-        defaultValue};
+    return std::apply(
+        [&](const I&... info) {
+          return StructArraySubscriber<T, I...>{
+              ::nt::Subscribe(
+                  m_handle, NT_RAW,
+                  wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(
+                      info...),
+                  options),
+              defaultValue, info...};
+        },
+        m_info);
   }
 
   /**
@@ -468,11 +541,17 @@
   SubscriberType Subscribe(
       std::span<const T> defaultValue,
       const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructArraySubscriber<T>{
-        ::nt::Subscribe(
-            m_handle, NT_RAW,
-            wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(), options),
-        defaultValue};
+    return std::apply(
+        [&](const I&... info) {
+          return StructArraySubscriber<T, I...>{
+              ::nt::Subscribe(
+                  m_handle, NT_RAW,
+                  wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(
+                      info...),
+                  options),
+              defaultValue, info...};
+        },
+        m_info);
   }
 
   /**
@@ -492,9 +571,17 @@
    */
   [[nodiscard]]
   PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructArrayPublisher<T>{::nt::Publish(
-        m_handle, NT_RAW,
-        wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(), options)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructArrayPublisher<T, I...>{
+              ::nt::Publish(
+                  m_handle, NT_RAW,
+                  wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(
+                      info...),
+                  options),
+              info...};
+        },
+        m_info);
   }
 
   /**
@@ -518,10 +605,17 @@
   PublisherType PublishEx(
       const wpi::json& properties,
       const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructArrayPublisher<T>{::nt::PublishEx(
-        m_handle, NT_RAW,
-        wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(), properties,
-        options)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructArrayPublisher<T, I...>{
+              ::nt::PublishEx(
+                  m_handle, NT_RAW,
+                  wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(
+                      info...),
+                  properties, options),
+              info...};
+        },
+        m_info);
   }
 
   /**
@@ -552,11 +646,17 @@
   [[nodiscard]]
   EntryType GetEntry(U&& defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructArrayEntry<T>{
-        ::nt::GetEntry(m_handle, NT_RAW,
-                       wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(),
-                       options),
-        defaultValue};
+    return std::apply(
+        [&](const I&... info) {
+          return StructArrayEntry<T, I...>{
+              ::nt::GetEntry(
+                  m_handle, NT_RAW,
+                  wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(
+                      info...),
+                  options),
+              defaultValue, info...};
+        },
+        m_info);
   }
 
   /**
@@ -582,12 +682,21 @@
   [[nodiscard]]
   EntryType GetEntry(std::span<const T> defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructArrayEntry<T>{
-        ::nt::GetEntry(m_handle, NT_RAW,
-                       wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(),
-                       options),
-        defaultValue};
+    return std::apply(
+        [&](const I&... info) {
+          return StructArrayEntry<T, I...>{
+              ::nt::GetEntry(
+                  m_handle, NT_RAW,
+                  wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(
+                      info...),
+                  options),
+              defaultValue, info...};
+        },
+        m_info);
   }
+
+ private:
+  [[no_unique_address]] std::tuple<I...> m_info;
 };
 
 }  // namespace nt
diff --git a/ntcore/src/main/native/include/networktables/StructTopic.h b/ntcore/src/main/native/include/networktables/StructTopic.h
index 88da9f3..b69d0af 100644
--- a/ntcore/src/main/native/include/networktables/StructTopic.h
+++ b/ntcore/src/main/native/include/networktables/StructTopic.h
@@ -8,36 +8,37 @@
 
 #include <atomic>
 #include <concepts>
+#include <functional>
 #include <span>
 #include <string_view>
+#include <tuple>
 #include <utility>
 #include <vector>
 
 #include <wpi/SmallVector.h>
+#include <wpi/json_fwd.h>
 #include <wpi/struct/Struct.h>
 
 #include "networktables/NetworkTableInstance.h"
 #include "networktables/Topic.h"
 #include "ntcore_cpp.h"
 
-namespace wpi {
-class json;
-}  // namespace wpi
-
 namespace nt {
 
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructTopic;
 
 /**
  * NetworkTables struct-encoded value subscriber.
  */
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructSubscriber : public Subscriber {
-  using S = wpi::Struct<T>;
+  using S = wpi::Struct<T, I...>;
 
  public:
-  using TopicType = StructTopic<T>;
+  using TopicType = StructTopic<T, I...>;
   using ValueType = T;
   using ParamType = const T&;
   using TimestampedValueType = Timestamped<T>;
@@ -50,9 +51,12 @@
    *
    * @param handle Native handle
    * @param defaultValue Default value
+   * @param info optional struct type info
    */
-  StructSubscriber(NT_Subscriber handle, T defaultValue)
-      : Subscriber{handle}, m_defaultValue{std::move(defaultValue)} {}
+  StructSubscriber(NT_Subscriber handle, T defaultValue, I... info)
+      : Subscriber{handle},
+        m_defaultValue{std::move(defaultValue)},
+        m_info{std::move(info)...} {}
 
   /**
    * Get the last published value.
@@ -84,12 +88,16 @@
    * @return true if successful
    */
   bool GetInto(T* out) {
-    wpi::SmallVector<uint8_t, S::kSize> buf;
+    wpi::SmallVector<uint8_t, 128> buf;
     TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
-    if (view.value.size() < S::kSize) {
+    if (view.value.size() < std::apply(S::GetSize, m_info)) {
       return false;
     } else {
-      wpi::UnpackStructInto(out, view.value.subspan<0, S::kSize>());
+      std::apply(
+          [&](const I&... info) {
+            wpi::UnpackStructInto(out, view.value, info...);
+          },
+          m_info);
       return true;
     }
   }
@@ -112,13 +120,16 @@
    * @return timestamped value
    */
   TimestampedValueType GetAtomic(const T& defaultValue) const {
-    wpi::SmallVector<uint8_t, S::kSize> buf;
+    wpi::SmallVector<uint8_t, 128> buf;
     TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
-    if (view.value.size() < S::kSize) {
+    if (view.value.size() < std::apply(S::GetSize, m_info)) {
       return {0, 0, defaultValue};
     } else {
-      return {view.time, view.serverTime,
-              S::Unpack(view.value.subspan<0, S::kSize>())};
+      return {
+          view.time, view.serverTime,
+          std::apply(
+              [&](const I&... info) { return S::Unpack(view.value, info...); },
+              m_info)};
     }
   }
 
@@ -138,13 +149,16 @@
     std::vector<TimestampedValueType> rv;
     rv.reserve(raw.size());
     for (auto&& r : raw) {
-      if (r.value.size() < S::kSize) {
+      if (r.value.size() < std::apply(S::GetSize, m_info)) {
         continue;
       } else {
-        rv.emplace_back(
-            r.time, r.serverTime,
-            S::Unpack(
-                std::span<const uint8_t>(r.value).subspan<0, S::kSize>()));
+        std::apply(
+            [&](const I&... info) {
+              rv.emplace_back(
+                  r.time, r.serverTime,
+                  S::Unpack(std::span<const uint8_t>(r.value), info...));
+            },
+            m_info);
       }
     }
     return rv;
@@ -156,22 +170,29 @@
    * @return Topic
    */
   TopicType GetTopic() const {
-    return StructTopic<T>{::nt::GetTopicFromHandle(m_subHandle)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructTopic<T, I...>{::nt::GetTopicFromHandle(m_subHandle),
+                                      info...};
+        },
+        m_info);
   }
 
  private:
   ValueType m_defaultValue;
+  [[no_unique_address]] std::tuple<I...> m_info;
 };
 
 /**
  * NetworkTables struct-encoded value publisher.
  */
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructPublisher : public Publisher {
-  using S = wpi::Struct<T>;
+  using S = wpi::Struct<T, I...>;
 
  public:
-  using TopicType = StructTopic<T>;
+  using TopicType = StructTopic<T, I...>;
   using ValueType = T;
   using ParamType = const T&;
 
@@ -183,14 +204,17 @@
   StructPublisher& operator=(const StructPublisher&) = delete;
 
   StructPublisher(StructPublisher&& rhs)
-      : Publisher{std::move(rhs)}, m_schemaPublished{rhs.m_schemaPublished} {}
+      : Publisher{std::move(rhs)},
+        m_schemaPublished{
+            rhs.m_schemaPublished.load(std::memory_order_relaxed)},
+        m_info{std::move(rhs.m_info)} {}
 
   StructPublisher& operator=(StructPublisher&& rhs) {
     Publisher::operator=(std::move(rhs));
-    m_schemaPublished.clear();
-    if (rhs.m_schemaPublished.test()) {
-      m_schemaPublished.test_and_set();
-    }
+    m_schemaPublished.store(
+        rhs.m_schemaPublished.load(std::memory_order_relaxed),
+        std::memory_order_relaxed);
+    m_info = std::move(rhs.m_info);
     return *this;
   }
 
@@ -199,8 +223,10 @@
    * StructTopic::Publish() instead.
    *
    * @param handle Native handle
+   * @param info optional struct type info
    */
-  explicit StructPublisher(NT_Publisher handle) : Publisher{handle} {}
+  explicit StructPublisher(NT_Publisher handle, I... info)
+      : Publisher{handle}, m_info{std::move(info)...} {}
 
   /**
    * Publish a new value.
@@ -209,11 +235,24 @@
    * @param time timestamp; 0 indicates current NT time should be used
    */
   void Set(const T& value, int64_t time = 0) {
-    if (!m_schemaPublished.test_and_set()) {
-      GetTopic().GetInstance().template AddStructSchema<T>();
+    if (!m_schemaPublished.exchange(true, std::memory_order_relaxed)) {
+      std::apply(
+          [&](const I&... info) {
+            GetTopic().GetInstance().template AddStructSchema<T>(info...);
+          },
+          m_info);
     }
-    uint8_t buf[S::kSize];
-    S::Pack(buf, value);
+    if constexpr (sizeof...(I) == 0) {
+      if constexpr (wpi::is_constexpr([] { S::GetSize(); })) {
+        uint8_t buf[S::GetSize()];
+        S::Pack(buf, value);
+        ::nt::SetRaw(m_pubHandle, buf, time);
+        return;
+      }
+    }
+    wpi::SmallVector<uint8_t, 128> buf;
+    buf.resize_for_overwrite(std::apply(S::GetSize, m_info));
+    std::apply([&](const I&... info) { S::Pack(buf, value, info...); }, m_info);
     ::nt::SetRaw(m_pubHandle, buf, time);
   }
 
@@ -225,11 +264,24 @@
    * @param value value
    */
   void SetDefault(const T& value) {
-    if (!m_schemaPublished.test_and_set()) {
-      GetTopic().GetInstance().template AddStructSchema<T>();
+    if (!m_schemaPublished.exchange(true, std::memory_order_relaxed)) {
+      std::apply(
+          [&](const I&... info) {
+            GetTopic().GetInstance().template AddStructSchema<T>(info...);
+          },
+          m_info);
     }
-    uint8_t buf[S::kSize];
-    S::Pack(buf, value);
+    if constexpr (sizeof...(I) == 0) {
+      if constexpr (wpi::is_constexpr([] { S::GetSize(); })) {
+        uint8_t buf[S::GetSize()];
+        S::Pack(buf, value);
+        ::nt::SetDefaultRaw(m_pubHandle, buf);
+        return;
+      }
+    }
+    wpi::SmallVector<uint8_t, 128> buf;
+    buf.resize_for_overwrite(std::apply(S::GetSize, m_info));
+    std::apply([&](const I&... info) { S::Pack(buf, value, info...); }, m_info);
     ::nt::SetDefaultRaw(m_pubHandle, buf);
   }
 
@@ -239,11 +291,17 @@
    * @return Topic
    */
   TopicType GetTopic() const {
-    return StructTopic<T>{::nt::GetTopicFromHandle(m_pubHandle)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructTopic<T, I...>{::nt::GetTopicFromHandle(m_pubHandle),
+                                      info...};
+        },
+        m_info);
   }
 
  private:
-  std::atomic_flag m_schemaPublished = ATOMIC_FLAG_INIT;
+  std::atomic_bool m_schemaPublished{false};
+  [[no_unique_address]] std::tuple<I...> m_info;
 };
 
 /**
@@ -251,13 +309,14 @@
  *
  * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
  */
-template <wpi::StructSerializable T>
-class StructEntry final : public StructSubscriber<T>,
-                          public StructPublisher<T> {
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
+class StructEntry final : public StructSubscriber<T, I...>,
+                          public StructPublisher<T, I...> {
  public:
-  using SubscriberType = StructSubscriber<T>;
-  using PublisherType = StructPublisher<T>;
-  using TopicType = StructTopic<T>;
+  using SubscriberType = StructSubscriber<T, I...>;
+  using PublisherType = StructPublisher<T, I...>;
+  using TopicType = StructTopic<T, I...>;
   using ValueType = T;
   using ParamType = const T&;
 
@@ -271,10 +330,11 @@
    *
    * @param handle Native handle
    * @param defaultValue Default value
+   * @param info optional struct type info
    */
-  StructEntry(NT_Entry handle, T defaultValue)
-      : StructSubscriber<T>{handle, std::move(defaultValue)},
-        StructPublisher<T>{handle} {}
+  StructEntry(NT_Entry handle, T defaultValue, const I&... info)
+      : StructSubscriber<T, I...>{handle, std::move(defaultValue), info...},
+        StructPublisher<T, I...>{handle, info...} {}
 
   /**
    * Determines if the native handle is valid.
@@ -295,9 +355,7 @@
    *
    * @return Topic
    */
-  TopicType GetTopic() const {
-    return StructTopic<T>{::nt::GetTopicFromHandle(this->m_subHandle)};
-  }
+  TopicType GetTopic() const { return StructSubscriber<T, I...>::GetTopic(); }
 
   /**
    * Stops publishing the entry if it's published.
@@ -308,12 +366,13 @@
 /**
  * NetworkTables struct-encoded value topic.
  */
-template <wpi::StructSerializable T>
+template <typename T, typename... I>
+  requires wpi::StructSerializable<T, I...>
 class StructTopic final : public Topic {
  public:
-  using SubscriberType = StructSubscriber<T>;
-  using PublisherType = StructPublisher<T>;
-  using EntryType = StructEntry<T>;
+  using SubscriberType = StructSubscriber<T, I...>;
+  using PublisherType = StructPublisher<T, I...>;
+  using EntryType = StructEntry<T, I...>;
   using ValueType = T;
   using ParamType = const T&;
   using TimestampedValueType = Timestamped<T>;
@@ -325,15 +384,19 @@
    * NetworkTableInstance::GetStructTopic() instead.
    *
    * @param handle Native handle
+   * @param info optional struct type info
    */
-  explicit StructTopic(NT_Topic handle) : Topic{handle} {}
+  explicit StructTopic(NT_Topic handle, I... info)
+      : Topic{handle}, m_info{std::move(info)...} {}
 
   /**
    * Construct from a generic topic.
    *
    * @param topic Topic
+   * @param info optional struct type info
    */
-  explicit StructTopic(Topic topic) : Topic{topic} {}
+  explicit StructTopic(Topic topic, I... info)
+      : Topic{topic}, m_info{std::move(info)...} {}
 
   /**
    * Create a new subscriber to the topic.
@@ -353,10 +416,15 @@
   [[nodiscard]]
   SubscriberType Subscribe(
       T defaultValue, const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructSubscriber<T>{
-        ::nt::Subscribe(m_handle, NT_RAW, wpi::GetStructTypeString<T>(),
-                        options),
-        std::move(defaultValue)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructSubscriber<T, I...>{
+              ::nt::Subscribe(m_handle, NT_RAW,
+                              wpi::GetStructTypeString<T, I...>(info...),
+                              options),
+              std::move(defaultValue), info...};
+        },
+        m_info);
   }
 
   /**
@@ -376,8 +444,15 @@
    */
   [[nodiscard]]
   PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructPublisher<T>{::nt::Publish(
-        m_handle, NT_RAW, wpi::GetStructTypeString<T>(), options)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructPublisher<T, I...>{
+              ::nt::Publish(m_handle, NT_RAW,
+                            wpi::GetStructTypeString<T, I...>(info...),
+                            options),
+              info...};
+        },
+        m_info);
   }
 
   /**
@@ -401,8 +476,15 @@
   PublisherType PublishEx(
       const wpi::json& properties,
       const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructPublisher<T>{::nt::PublishEx(
-        m_handle, NT_RAW, wpi::GetStructTypeString<T>(), properties, options)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructPublisher<T, I...>{
+              ::nt::PublishEx(m_handle, NT_RAW,
+                              wpi::GetStructTypeString<T, I...>(info...),
+                              properties, options),
+              info...};
+        },
+        m_info);
   }
 
   /**
@@ -428,11 +510,19 @@
   [[nodiscard]]
   EntryType GetEntry(T defaultValue,
                      const PubSubOptions& options = kDefaultPubSubOptions) {
-    return StructEntry<T>{
-        ::nt::GetEntry(m_handle, NT_RAW, wpi::GetStructTypeString<T>(),
-                       options),
-        std::move(defaultValue)};
+    return std::apply(
+        [&](const I&... info) {
+          return StructEntry<T, I...>{
+              ::nt::GetEntry(m_handle, NT_RAW,
+                             wpi::GetStructTypeString<T, I...>(info...),
+                             options),
+              std::move(defaultValue), info...};
+        },
+        m_info);
   }
+
+ private:
+  [[no_unique_address]] std::tuple<I...> m_info;
 };
 
 }  // namespace nt
diff --git a/ntcore/src/main/native/include/networktables/Topic.h b/ntcore/src/main/native/include/networktables/Topic.h
index d623fbd..eedd487 100644
--- a/ntcore/src/main/native/include/networktables/Topic.h
+++ b/ntcore/src/main/native/include/networktables/Topic.h
@@ -103,6 +103,21 @@
   bool IsRetained() const;
 
   /**
+   * Allow storage of the topic's last value, allowing the value to be read (and
+   * not just accessed through event queues and listeners).
+   *
+   * @param cached True for cached, false for not cached.
+   */
+  void SetCached(bool cached);
+
+  /**
+   * Returns whether the topic's last value is stored.
+   *
+   * @return True if the topic is cached.
+   */
+  bool IsCached() const;
+
+  /**
    * Determines if the topic is currently being published.
    *
    * @return True if the topic exists, false otherwise.
diff --git a/ntcore/src/main/native/include/networktables/Topic.inc b/ntcore/src/main/native/include/networktables/Topic.inc
index 166dc6d..605f9d8 100644
--- a/ntcore/src/main/native/include/networktables/Topic.inc
+++ b/ntcore/src/main/native/include/networktables/Topic.inc
@@ -41,6 +41,14 @@
   return ::nt::GetTopicRetained(m_handle);
 }
 
+inline void Topic::SetCached(bool cached) {
+  ::nt::SetTopicCached(m_handle, cached);
+}
+
+inline bool Topic::IsCached() const {
+  return ::nt::GetTopicCached(m_handle);
+}
+
 inline bool Topic::Exists() const {
   return nt::GetTopicExists(m_handle);
 }
diff --git a/ntcore/src/main/native/include/ntcore_c.h b/ntcore/src/main/native/include/ntcore_c.h
index 1af0e66..a335f81 100644
--- a/ntcore/src/main/native/include/ntcore_c.h
+++ b/ntcore/src/main/native/include/ntcore_c.h
@@ -65,7 +65,11 @@
 };
 
 /** NetworkTables entry flags. */
-enum NT_EntryFlags { NT_PERSISTENT = 0x01, NT_RETAINED = 0x02 };
+enum NT_EntryFlags {
+  NT_PERSISTENT = 0x01,
+  NT_RETAINED = 0x02,
+  NT_UNCACHED = 0x04
+};
 
 /** NetworkTables logging levels. */
 enum NT_LogLevel {
@@ -686,6 +690,24 @@
 NT_Bool NT_GetTopicRetained(NT_Topic topic);
 
 /**
+ * Sets the cached property of a topic.  If true, the server and clients will
+ * store the latest value, allowing the value to be read (and not just accessed
+ * through event queues and listeners).
+ *
+ * @param topic topic handle
+ * @param value True for cached, false for not cached
+ */
+void NT_SetTopicCached(NT_Topic topic, NT_Bool value);
+
+/**
+ * Gets the cached property of a topic.
+ *
+ * @param topic topic handle
+ * @return cached property value
+ */
+NT_Bool NT_GetTopicCached(NT_Topic topic);
+
+/**
  * Determine if topic exists (e.g. has at least one publisher).
  *
  * @param handle Topic, entry, or subscriber handle.
diff --git a/ntcore/src/main/native/include/ntcore_cpp.h b/ntcore/src/main/native/include/ntcore_cpp.h
index 482d1e3..e2529e1 100644
--- a/ntcore/src/main/native/include/ntcore_cpp.h
+++ b/ntcore/src/main/native/include/ntcore_cpp.h
@@ -682,6 +682,24 @@
 bool GetTopicRetained(NT_Topic topic);
 
 /**
+ * Sets the cached property of a topic.  If true, the server and clients will
+ * store the latest value, allowing the value to be read (and not just accessed
+ * through event queues and listeners).
+ *
+ * @param topic topic handle
+ * @param value True for cached, false for not cached
+ */
+void SetTopicCached(NT_Topic topic, bool value);
+
+/**
+ * Gets the cached property of a topic.
+ *
+ * @param topic topic handle
+ * @return cached property value
+ */
+bool GetTopicCached(NT_Topic topic);
+
+/**
  * Determine if topic exists (e.g. has at least one publisher).
  *
  * @param handle Topic, entry, or subscriber handle.
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java
index 4c431af..bca694b 100644
--- a/ntcore/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java
@@ -6,7 +6,6 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Stream;
@@ -61,8 +60,8 @@
     return Stream.of(
         Arguments.of(Collections.singletonList("/"), ""),
         Arguments.of(Collections.singletonList("/"), "/"),
-        Arguments.of(Arrays.asList("/", "/foo", "/foo/bar", "/foo/bar/baz"), "/foo/bar/baz"),
-        Arguments.of(Arrays.asList("/", "/foo", "/foo/bar", "/foo/bar/"), "/foo/bar/"));
+        Arguments.of(List.of("/", "/foo", "/foo/bar", "/foo/bar/baz"), "/foo/bar/baz"),
+        Arguments.of(List.of("/", "/foo", "/foo/bar", "/foo/bar/"), "/foo/bar/"));
   }
 
   @ParameterizedTest
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/RawTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/RawTest.java
index 73d5efb..293d443 100644
--- a/ntcore/src/test/java/edu/wpi/first/networktables/RawTest.java
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/RawTest.java
@@ -4,11 +4,10 @@
 
 package edu.wpi.first.networktables;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.nio.ByteBuffer;
-import java.util.Arrays;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -31,9 +30,9 @@
   void testGenericByteArray() {
     GenericEntry entry = m_inst.getTopic("test").getGenericEntry("raw");
     entry.setRaw(new byte[] {5}, 10);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {5}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {5});
     entry.setRaw(new byte[] {5, 6, 7}, 1, 2, 15);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {6, 7}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {6, 7});
     assertThrows(IndexOutOfBoundsException.class, () -> entry.setRaw(new byte[] {5}, -1, 2, 20));
     assertThrows(IndexOutOfBoundsException.class, () -> entry.setRaw(new byte[] {5}, 1, -2, 20));
     assertThrows(IndexOutOfBoundsException.class, () -> entry.setRaw(new byte[] {5}, 1, 1, 20));
@@ -43,9 +42,9 @@
   void testRawByteArray() {
     RawEntry entry = m_inst.getRawTopic("test").getEntry("raw", new byte[] {});
     entry.set(new byte[] {5}, 10);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {5}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {5});
     entry.set(new byte[] {5, 6, 7}, 1, 2, 15);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {6, 7}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {6, 7});
     assertThrows(IndexOutOfBoundsException.class, () -> entry.set(new byte[] {5}, -1, 1, 20));
     assertThrows(IndexOutOfBoundsException.class, () -> entry.set(new byte[] {5}, 1, -1, 20));
     assertThrows(IndexOutOfBoundsException.class, () -> entry.set(new byte[] {5}, 1, 1, 20));
@@ -55,15 +54,15 @@
   void testGenericByteBuffer() {
     GenericEntry entry = m_inst.getTopic("test").getGenericEntry("raw");
     entry.setRaw(ByteBuffer.wrap(new byte[] {5}), 10);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {5}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {5});
     entry.setRaw(ByteBuffer.wrap(new byte[] {5, 6, 7}).position(1), 15);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {6, 7}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {6, 7});
     entry.setRaw(ByteBuffer.wrap(new byte[] {5, 6, 7}).position(1).limit(2), 16);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {6}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {6});
     entry.setRaw(ByteBuffer.wrap(new byte[] {8, 9, 0}), 1, 2, 20);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {9, 0}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {9, 0});
     entry.setRaw(ByteBuffer.wrap(new byte[] {1, 2, 3}).position(2), 0, 2, 25);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {1, 2}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {1, 2});
     assertThrows(
         IndexOutOfBoundsException.class,
         () -> entry.setRaw(ByteBuffer.wrap(new byte[] {5}), -1, 1, 30));
@@ -79,15 +78,15 @@
   void testRawByteBuffer() {
     RawEntry entry = m_inst.getRawTopic("test").getEntry("raw", new byte[] {});
     entry.set(ByteBuffer.wrap(new byte[] {5}), 10);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {5}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {5});
     entry.set(ByteBuffer.wrap(new byte[] {5, 6, 7}).position(1), 15);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {6, 7}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {6, 7});
     entry.set(ByteBuffer.wrap(new byte[] {5, 6, 7}).position(1).limit(2), 16);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {6}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {6});
     entry.set(ByteBuffer.wrap(new byte[] {8, 9, 0}), 1, 2, 20);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {9, 0}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {9, 0});
     entry.set(ByteBuffer.wrap(new byte[] {1, 2, 3}).position(2), 0, 2, 25);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {1, 2}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {1, 2});
     assertThrows(
         IndexOutOfBoundsException.class,
         () -> entry.set(ByteBuffer.wrap(new byte[] {5}), -1, 1, 30));
@@ -105,13 +104,13 @@
     ByteBuffer bb = ByteBuffer.allocateDirect(3);
     bb.put(new byte[] {5, 6, 7});
     entry.setRaw(bb.position(1), 15);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {6, 7}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {6, 7});
     entry.setRaw(bb.limit(2), 16);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {6}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {6});
     bb.clear();
     bb.put(new byte[] {8, 9, 0});
     entry.setRaw(bb, 1, 2, 20);
-    assertTrue(Arrays.equals(entry.getRaw(new byte[] {}), new byte[] {9, 0}));
+    assertArrayEquals(entry.getRaw(new byte[] {}), new byte[] {9, 0});
     assertThrows(IndexOutOfBoundsException.class, () -> entry.setRaw(bb, -1, 1, 25));
     assertThrows(IndexOutOfBoundsException.class, () -> entry.setRaw(bb, 1, -1, 25));
     assertThrows(IndexOutOfBoundsException.class, () -> entry.setRaw(bb, 2, 2, 25));
@@ -123,13 +122,13 @@
     ByteBuffer bb = ByteBuffer.allocateDirect(3);
     bb.put(new byte[] {5, 6, 7});
     entry.set(bb.position(1), 15);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {6, 7}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {6, 7});
     entry.set(bb.limit(2), 16);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {6}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {6});
     bb.clear();
     bb.put(new byte[] {8, 9, 0});
     entry.set(bb, 1, 2, 20);
-    assertTrue(Arrays.equals(entry.get(new byte[] {}), new byte[] {9, 0}));
+    assertArrayEquals(entry.get(new byte[] {}), new byte[] {9, 0});
     assertThrows(IndexOutOfBoundsException.class, () -> entry.set(bb, -1, 1, 25));
     assertThrows(IndexOutOfBoundsException.class, () -> entry.set(bb, 1, -1, 25));
     assertThrows(IndexOutOfBoundsException.class, () -> entry.set(bb, 2, 2, 25));
diff --git a/ntcore/src/test/native/cpp/LocalStorageTest.cpp b/ntcore/src/test/native/cpp/LocalStorageTest.cpp
index eb57913..cdb5272 100644
--- a/ntcore/src/test/native/cpp/LocalStorageTest.cpp
+++ b/ntcore/src/test/native/cpp/LocalStorageTest.cpp
@@ -101,6 +101,17 @@
   EXPECT_FALSE(storage.GetTopicExists(fooTopic));
 }
 
+TEST_F(LocalStorageTest, DefaultProps) {
+  EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+                               std::string_view{"boolean"}, wpi::json::object(),
+                               IsDefaultPubSubOptions()));
+  storage.Publish(fooTopic, NT_BOOLEAN, "boolean", wpi::json::object(), {});
+
+  EXPECT_FALSE(storage.GetTopicPersistent(fooTopic));
+  EXPECT_FALSE(storage.GetTopicRetained(fooTopic));
+  EXPECT_TRUE(storage.GetTopicCached(fooTopic));
+}
+
 TEST_F(LocalStorageTest, PublishNewNoProps) {
   EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
                                std::string_view{"boolean"}, wpi::json::object(),
diff --git a/ntcore/src/test/native/cpp/StructTest.cpp b/ntcore/src/test/native/cpp/StructTest.cpp
new file mode 100644
index 0000000..55b6a90
--- /dev/null
+++ b/ntcore/src/test/native/cpp/StructTest.cpp
@@ -0,0 +1,450 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <gtest/gtest.h>
+#include <wpi/SpanMatcher.h>
+#include <wpi/struct/Struct.h>
+
+#include "networktables/NetworkTableInstance.h"
+#include "networktables/StructArrayTopic.h"
+#include "networktables/StructTopic.h"
+
+namespace {
+struct Inner {
+  int a = 0;
+  int b = 0;
+};
+
+struct Outer {
+  Inner inner;
+  int c = 0;
+};
+
+struct Inner2 {
+  int a = 0;
+  int b = 0;
+};
+
+struct Outer2 {
+  Inner2 inner;
+  int c = 0;
+};
+
+struct ThingA {
+  int x = 0;
+};
+
+struct ThingB {
+  int x = 0;
+};
+
+struct Info1 {
+  int info = 0;
+};
+}  // namespace
+
+template <>
+struct wpi::Struct<Inner> {
+  static constexpr std::string_view GetTypeString() { return "struct:Inner"; }
+  static constexpr size_t GetSize() { return 8; }
+  static constexpr std::string_view GetSchema() { return "int32 a; int32 b"; }
+
+  static Inner Unpack(std::span<const uint8_t> data) {
+    return {wpi::UnpackStruct<int32_t, 0>(data),
+            wpi::UnpackStruct<int32_t, 4>(data)};
+  }
+  static void Pack(std::span<uint8_t> data, const Inner& value) {
+    wpi::PackStruct<0>(data, value.a);
+    wpi::PackStruct<4>(data, value.b);
+  }
+};
+
+template <>
+struct wpi::Struct<Outer> {
+  static constexpr std::string_view GetTypeString() { return "struct:Outer"; }
+  static constexpr size_t GetSize() { return wpi::GetStructSize<Inner>() + 4; }
+  static constexpr std::string_view GetSchema() {
+    return "Inner inner; int32 c";
+  }
+
+  static Outer Unpack(std::span<const uint8_t> data) {
+    constexpr size_t innerSize = wpi::GetStructSize<Inner>();
+    return {wpi::UnpackStruct<Inner, 0>(data),
+            wpi::UnpackStruct<int32_t, innerSize>(data)};
+  }
+  static void Pack(std::span<uint8_t> data, const Outer& value) {
+    constexpr size_t innerSize = wpi::GetStructSize<Inner>();
+    wpi::PackStruct<0>(data, value.inner);
+    wpi::PackStruct<innerSize>(data, value.c);
+  }
+  static void ForEachNested(
+      std::invocable<std::string_view, std::string_view> auto fn) {
+    wpi::ForEachStructSchema<Inner>(fn);
+  }
+};
+
+template <>
+struct wpi::Struct<Inner2> {
+  static std::string_view GetTypeString() { return "struct:Inner2"; }
+  static size_t GetSize() { return 8; }
+  static std::string_view GetSchema() { return "int32 a; int32 b"; }
+
+  static Inner2 Unpack(std::span<const uint8_t> data) {
+    return {wpi::UnpackStruct<int32_t, 0>(data),
+            wpi::UnpackStruct<int32_t, 4>(data)};
+  }
+  static void Pack(std::span<uint8_t> data, const Inner2& value) {
+    wpi::PackStruct<0>(data, value.a);
+    wpi::PackStruct<4>(data, value.b);
+  }
+};
+
+template <>
+struct wpi::Struct<Outer2> {
+  static std::string_view GetTypeString() { return "struct:Outer2"; }
+  static size_t GetSize() { return wpi::GetStructSize<Inner>() + 4; }
+  static std::string_view GetSchema() { return "Inner2 inner; int32 c"; }
+
+  static Outer2 Unpack(std::span<const uint8_t> data) {
+    size_t innerSize = wpi::GetStructSize<Inner2>();
+    return {wpi::UnpackStruct<Inner2, 0>(data),
+            wpi::UnpackStruct<int32_t>(data.subspan(innerSize))};
+  }
+  static void Pack(std::span<uint8_t> data, const Outer2& value) {
+    size_t innerSize = wpi::GetStructSize<Inner2>();
+    wpi::PackStruct<0>(data, value.inner);
+    wpi::PackStruct(data.subspan(innerSize), value.c);
+  }
+  static void ForEachNested(
+      std::invocable<std::string_view, std::string_view> auto fn) {
+    wpi::ForEachStructSchema<Inner2>(fn);
+  }
+};
+
+template <>
+struct wpi::Struct<ThingA> {
+  static constexpr std::string_view GetTypeString() { return "struct:ThingA"; }
+  static constexpr size_t GetSize() { return 1; }
+  static constexpr std::string_view GetSchema() { return "uint8 value"; }
+  static ThingA Unpack(std::span<const uint8_t> data) {
+    return ThingA{.x = data[0]};
+  }
+  static void Pack(std::span<uint8_t> data, const ThingA& value) {
+    data[0] = value.x;
+  }
+};
+
+template <>
+struct wpi::Struct<ThingB, Info1> {
+  static constexpr std::string_view GetTypeString(const Info1&) {
+    return "struct:ThingB";
+  }
+  static constexpr size_t GetSize(const Info1&) { return 1; }
+  static constexpr std::string_view GetSchema(const Info1&) {
+    return "uint8 value";
+  }
+  static ThingB Unpack(std::span<const uint8_t> data, const Info1&) {
+    return ThingB{.x = data[0]};
+  }
+  static void Pack(std::span<uint8_t> data, const ThingB& value, const Info1&) {
+    data[0] = value.x;
+  }
+};
+
+namespace nt {
+
+class StructTest : public ::testing::Test {
+ public:
+  StructTest() { inst = nt::NetworkTableInstance::Create(); }
+  ~StructTest() { nt::NetworkTableInstance::Destroy(inst); }
+
+  nt::NetworkTableInstance inst;
+};
+
+TEST_F(StructTest, InnerConstexpr) {
+  nt::StructTopic<Inner> topic = inst.GetStructTopic<Inner>("inner");
+  nt::StructPublisher<Inner> pub = topic.Publish();
+  nt::StructSubscriber<Inner> sub = topic.Subscribe({});
+
+  ASSERT_EQ(topic.GetTypeString(), "struct:Inner");
+
+  pub.SetDefault({0, 1});
+  Inner val = sub.Get();
+  ASSERT_EQ(val.a, 0);
+  ASSERT_EQ(val.b, 1);
+
+  pub.Set({1, 2});
+  auto atomicVal = sub.GetAtomic();
+  ASSERT_EQ(atomicVal.value.a, 1);
+  ASSERT_EQ(atomicVal.value.b, 2);
+
+  Inner val2;
+  sub.GetInto(&val2);
+  ASSERT_EQ(val2.a, 1);
+  ASSERT_EQ(val2.b, 2);
+
+  auto vals = sub.ReadQueue();
+  ASSERT_EQ(vals.size(), 1u);
+  ASSERT_EQ(vals[0].value.a, 1);
+  ASSERT_EQ(vals[0].value.b, 2);
+}
+
+TEST_F(StructTest, InnerNonconstexpr) {
+  nt::StructTopic<Inner2> topic = inst.GetStructTopic<Inner2>("inner2");
+  nt::StructPublisher<Inner2> pub = topic.Publish();
+  nt::StructSubscriber<Inner2> sub = topic.Subscribe({});
+
+  ASSERT_EQ(topic.GetTypeString(), "struct:Inner2");
+
+  pub.SetDefault({0, 1});
+  Inner2 val = sub.Get();
+  ASSERT_EQ(val.a, 0);
+  ASSERT_EQ(val.b, 1);
+
+  pub.Set({1, 2});
+  auto atomicVal = sub.GetAtomic();
+  ASSERT_EQ(atomicVal.value.a, 1);
+  ASSERT_EQ(atomicVal.value.b, 2);
+
+  Inner2 val2;
+  sub.GetInto(&val2);
+  ASSERT_EQ(val2.a, 1);
+  ASSERT_EQ(val2.b, 2);
+
+  auto vals = sub.ReadQueue();
+  ASSERT_EQ(vals.size(), 1u);
+  ASSERT_EQ(vals[0].value.a, 1);
+  ASSERT_EQ(vals[0].value.b, 2);
+}
+
+TEST_F(StructTest, OuterConstexpr) {
+  nt::StructTopic<Outer> topic = inst.GetStructTopic<Outer>("outer");
+  nt::StructPublisher<Outer> pub = topic.Publish();
+  nt::StructSubscriber<Outer> sub = topic.Subscribe({});
+
+  ASSERT_EQ(topic.GetTypeString(), "struct:Outer");
+
+  pub.SetDefault({{0, 1}, 2});
+  Outer val = sub.Get();
+  ASSERT_EQ(val.inner.a, 0);
+  ASSERT_EQ(val.inner.b, 1);
+  ASSERT_EQ(val.c, 2);
+
+  pub.Set({{1, 2}, 3});
+  auto atomicVal = sub.GetAtomic();
+  ASSERT_EQ(atomicVal.value.inner.a, 1);
+  ASSERT_EQ(atomicVal.value.inner.b, 2);
+  ASSERT_EQ(atomicVal.value.c, 3);
+
+  Outer val2;
+  sub.GetInto(&val2);
+  ASSERT_EQ(val2.inner.a, 1);
+  ASSERT_EQ(val2.inner.b, 2);
+  ASSERT_EQ(val2.c, 3);
+
+  auto vals = sub.ReadQueue();
+  ASSERT_EQ(vals.size(), 1u);
+  ASSERT_EQ(vals[0].value.inner.a, 1);
+  ASSERT_EQ(vals[0].value.inner.b, 2);
+  ASSERT_EQ(vals[0].value.c, 3);
+}
+
+TEST_F(StructTest, OuterNonconstexpr) {
+  nt::StructTopic<Outer2> topic = inst.GetStructTopic<Outer2>("outer2");
+  nt::StructPublisher<Outer2> pub = topic.Publish();
+  nt::StructSubscriber<Outer2> sub = topic.Subscribe({});
+
+  ASSERT_EQ(topic.GetTypeString(), "struct:Outer2");
+
+  pub.SetDefault({{0, 1}, 2});
+  Outer2 val = sub.Get();
+  ASSERT_EQ(val.inner.a, 0);
+  ASSERT_EQ(val.inner.b, 1);
+  ASSERT_EQ(val.c, 2);
+
+  pub.Set({{1, 2}, 3});
+  auto atomicVal = sub.GetAtomic();
+  ASSERT_EQ(atomicVal.value.inner.a, 1);
+  ASSERT_EQ(atomicVal.value.inner.b, 2);
+  ASSERT_EQ(atomicVal.value.c, 3);
+
+  Outer2 val2;
+  sub.GetInto(&val2);
+  ASSERT_EQ(val2.inner.a, 1);
+  ASSERT_EQ(val2.inner.b, 2);
+  ASSERT_EQ(val2.c, 3);
+
+  auto vals = sub.ReadQueue();
+  ASSERT_EQ(vals.size(), 1u);
+  ASSERT_EQ(vals[0].value.inner.a, 1);
+  ASSERT_EQ(vals[0].value.inner.b, 2);
+  ASSERT_EQ(vals[0].value.c, 3);
+}
+
+TEST_F(StructTest, InnerArrayConstexpr) {
+  nt::StructArrayTopic<Inner> topic = inst.GetStructArrayTopic<Inner>("innerA");
+  nt::StructArrayPublisher<Inner> pub = topic.Publish();
+  nt::StructArraySubscriber<Inner> sub = topic.Subscribe({});
+
+  ASSERT_EQ(topic.GetTypeString(), "struct:Inner[]");
+
+  pub.SetDefault({{{0, 1}}});
+  auto val = sub.Get();
+  ASSERT_EQ(val.size(), 1u);
+  ASSERT_EQ(val[0].a, 0);
+  ASSERT_EQ(val[0].b, 1);
+
+  pub.Set({{{1, 2}}});
+  auto atomicVal = sub.GetAtomic();
+  ASSERT_EQ(atomicVal.value.size(), 1u);
+  ASSERT_EQ(atomicVal.value[0].a, 1);
+  ASSERT_EQ(atomicVal.value[0].b, 2);
+
+  auto vals = sub.ReadQueue();
+  ASSERT_EQ(vals.size(), 1u);
+  ASSERT_EQ(vals[0].value.size(), 1u);
+  ASSERT_EQ(vals[0].value[0].a, 1);
+  ASSERT_EQ(vals[0].value[0].b, 2);
+}
+
+TEST_F(StructTest, InnerArrayNonconstexpr) {
+  nt::StructArrayTopic<Inner2> topic =
+      inst.GetStructArrayTopic<Inner2>("innerA2");
+  nt::StructArrayPublisher<Inner2> pub = topic.Publish();
+  nt::StructArraySubscriber<Inner2> sub = topic.Subscribe({});
+
+  ASSERT_EQ(topic.GetTypeString(), "struct:Inner2[]");
+
+  pub.SetDefault({{{0, 1}}});
+  auto val = sub.Get();
+  ASSERT_EQ(val.size(), 1u);
+  ASSERT_EQ(val[0].a, 0);
+  ASSERT_EQ(val[0].b, 1);
+
+  pub.Set({{{1, 2}}});
+  auto atomicVal = sub.GetAtomic();
+  ASSERT_EQ(atomicVal.value.size(), 1u);
+  ASSERT_EQ(atomicVal.value[0].a, 1);
+  ASSERT_EQ(atomicVal.value[0].b, 2);
+
+  auto vals = sub.ReadQueue();
+  ASSERT_EQ(vals.size(), 1u);
+  ASSERT_EQ(vals[0].value.size(), 1u);
+  ASSERT_EQ(vals[0].value[0].a, 1);
+  ASSERT_EQ(vals[0].value[0].b, 2);
+}
+
+TEST_F(StructTest, StructA) {
+  nt::StructTopic<ThingA> topic = inst.GetStructTopic<ThingA>("a");
+  nt::StructPublisher<ThingA> pub = topic.Publish();
+  nt::StructPublisher<ThingA> pub2 = topic.PublishEx({{}});
+  nt::StructSubscriber<ThingA> sub = topic.Subscribe({});
+  nt::StructEntry<ThingA> entry = topic.GetEntry({});
+  pub.SetDefault({});
+  pub.Set({}, 5);
+  sub.Get();
+  sub.Get({});
+  sub.GetAtomic();
+  sub.GetAtomic({});
+  entry.SetDefault({});
+  entry.Set({}, 6);
+  entry.Get({});
+}
+
+TEST_F(StructTest, StructArrayA) {
+  nt::StructArrayTopic<ThingA> topic = inst.GetStructArrayTopic<ThingA>("a");
+  nt::StructArrayPublisher<ThingA> pub = topic.Publish();
+  nt::StructArrayPublisher<ThingA> pub2 = topic.PublishEx({{}});
+  nt::StructArraySubscriber<ThingA> sub = topic.Subscribe({});
+  nt::StructArrayEntry<ThingA> entry = topic.GetEntry({});
+  pub.SetDefault({{ThingA{}, ThingA{}}});
+  pub.Set({{ThingA{}, ThingA{}}}, 5);
+  sub.Get();
+  sub.Get({});
+  sub.GetAtomic();
+  sub.GetAtomic({});
+  entry.SetDefault({{ThingA{}, ThingA{}}});
+  entry.Set({{ThingA{}, ThingA{}}}, 6);
+  entry.Get({});
+}
+
+TEST_F(StructTest, StructFixedArrayA) {
+  nt::StructTopic<std::array<ThingA, 2>> topic =
+      inst.GetStructTopic<std::array<ThingA, 2>>("a");
+  nt::StructPublisher<std::array<ThingA, 2>> pub = topic.Publish();
+  nt::StructPublisher<std::array<ThingA, 2>> pub2 = topic.PublishEx({{}});
+  nt::StructSubscriber<std::array<ThingA, 2>> sub = topic.Subscribe({});
+  nt::StructEntry<std::array<ThingA, 2>> entry = topic.GetEntry({});
+  std::array<ThingA, 2> arr;
+  pub.SetDefault(arr);
+  pub.Set(arr, 5);
+  sub.Get();
+  sub.Get(arr);
+  sub.GetAtomic();
+  sub.GetAtomic(arr);
+  entry.SetDefault(arr);
+  entry.Set(arr, 6);
+  entry.Get(arr);
+}
+
+TEST_F(StructTest, StructB) {
+  Info1 info;
+  nt::StructTopic<ThingB, Info1> topic =
+      inst.GetStructTopic<ThingB, Info1>("b", info);
+  nt::StructPublisher<ThingB, Info1> pub = topic.Publish();
+  nt::StructPublisher<ThingB, Info1> pub2 = topic.PublishEx({{}});
+  nt::StructSubscriber<ThingB, Info1> sub = topic.Subscribe({});
+  nt::StructEntry<ThingB, Info1> entry = topic.GetEntry({});
+  pub.SetDefault({});
+  pub.Set({}, 5);
+  sub.Get();
+  sub.Get({});
+  sub.GetAtomic();
+  sub.GetAtomic({});
+  entry.SetDefault({});
+  entry.Set({}, 6);
+  entry.Get({});
+}
+
+TEST_F(StructTest, StructArrayB) {
+  Info1 info;
+  nt::StructArrayTopic<ThingB, Info1> topic =
+      inst.GetStructArrayTopic<ThingB, Info1>("b", info);
+  nt::StructArrayPublisher<ThingB, Info1> pub = topic.Publish();
+  nt::StructArrayPublisher<ThingB, Info1> pub2 = topic.PublishEx({{}});
+  nt::StructArraySubscriber<ThingB, Info1> sub = topic.Subscribe({});
+  nt::StructArrayEntry<ThingB, Info1> entry = topic.GetEntry({});
+  pub.SetDefault({{ThingB{}, ThingB{}}});
+  pub.Set({{ThingB{}, ThingB{}}}, 5);
+  sub.Get();
+  sub.Get({});
+  sub.GetAtomic();
+  sub.GetAtomic({});
+  entry.SetDefault({{ThingB{}, ThingB{}}});
+  entry.Set({{ThingB{}, ThingB{}}}, 6);
+  entry.Get({});
+}
+
+TEST_F(StructTest, StructFixedArrayB) {
+  Info1 info;
+  nt::StructTopic<std::array<ThingB, 2>, Info1> topic =
+      inst.GetStructTopic<std::array<ThingB, 2>, Info1>("b", info);
+  nt::StructPublisher<std::array<ThingB, 2>, Info1> pub = topic.Publish();
+  nt::StructPublisher<std::array<ThingB, 2>, Info1> pub2 =
+      topic.PublishEx({{}});
+  nt::StructSubscriber<std::array<ThingB, 2>, Info1> sub = topic.Subscribe({});
+  nt::StructEntry<std::array<ThingB, 2>, Info1> entry = topic.GetEntry({});
+  std::array<ThingB, 2> arr;
+  pub.SetDefault(arr);
+  pub.Set(arr, 5);
+  sub.Get();
+  sub.Get(arr);
+  sub.GetAtomic();
+  sub.GetAtomic(arr);
+  entry.SetDefault(arr);
+  entry.Set(arr, 6);
+  entry.Get(arr);
+}
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/main.cpp b/ntcore/src/test/native/cpp/main.cpp
index 0f060b0..20bd583 100644
--- a/ntcore/src/test/native/cpp/main.cpp
+++ b/ntcore/src/test/native/cpp/main.cpp
@@ -10,7 +10,7 @@
 #include "ntcore.h"
 
 int main(int argc, char** argv) {
-  wpi::impl::SetupNowRio();
+  wpi::impl::SetupNowDefaultOnRio();
   nt::AddLogger(nt::GetDefaultInstance(), 0, UINT_MAX, [](auto& event) {
     if (auto msg = event.GetLogMessage()) {
       std::fputs(msg->message.c_str(), stderr);