Repository navigation
Architecture
Note
Current state: milestone M0 (scaffold). The Gradle modules, build, CI, server config spec, API holder and architecture tests exist. The simulation itself (SimulationWorld, physics, sensors, networking) is design only. Sections below mark what is implemented and what is planned. See Roadmap.
Prompt Critical separates a pure-Java simulation domain from the Minecraft binding. The simulation can be unit-tested and benchmarked without starting the game, and Minecraft code cannot leak into physics.
| # | Decision | Alternative | Why |
|---|---|---|---|
| A1 | Simulation is a separate module pc-sim, pure Java 21, no Minecraft dependencies |
Simulation classes inside the mod | Testable with plain JUnit, benchmarkable with JMH |
| A2 | One SimulationWorld per ServerLevel (dimension) |
One per server | Stations never span dimensions; load/save isolated per dimension SavedData
|
| A3 | FAST domain on the server thread; SLOW/STORAGE and the radiation field on worker threads with deterministic merge at a tick boundary | Everything on a separate thread | Exact coupling with Create kinetics; determinism |
| A4 | Structure-of-arrays stores on primitive arrays | Object graph | Zero allocations in the hot loop, cache locality |
| A5 |
SimSnapshot that readers see as immutable, from a buffer pool with optimistic (seqlock) validation |
Locks / copy every tick | Lock-free reads from CC and network threads |
| A6 | Every external action is a SimCommand; every consequence is a SimEvent
|
Direct calls | Deterministic ordering, journaling, replay |
| A7 | Clients receive only quantized deltas of subscribed channels | Full block entity sync | 2 KB/s per player budget |
+----------+
| pc-mod |--> NeoForge, Minecraft, Create, CC: Tweaked (optional)
+----+-----+
| depends
+-------+-------+
v v
+---------+ +---------+
| pc-api |---->| pc-sim |--> java.base only
+---------+ +---------+
| Module | Plugin | Role | Today |
|---|---|---|---|
pc-sim |
java-library + JMH (me.champeau.jmh) |
Simulation domain. No runtime dependencies; test deps only (JUnit 5, jqwik, ArchUnit, AssertJ) |
sim.core.SimClock, sim.core.TimeDomain, architecture tests, SimClockTest
|
pc-api |
java-library (api(project(":pc-sim"))) |
Stable interfaces for addons |
PromptCriticalApi (apiVersion(), CURRENT_API_VERSION = 1), ApiHolder
|
pc-mod |
ModDevGradle (net.neoforged.moddev) |
NeoForge binding: blocks, client, network, integrations |
@Mod class PromptCritical, ServerConfig, ServerMode, DecayHeatClock, bootstrap GameTests, ~270 placeholder block models and textures |
pc-sim and pc-api are packed into the mod jar as classes, not jar-in-jar: one mod, one classloader, no version conflicts. In dev runs both source sets are registered as part of the mod. The design also plans to publish pc-api separately to Maven as a compileOnly artifact for addon developers.
Mod metadata (neoforge.mods.toml): neoforge and minecraft required, create required (ordering AFTER), computercraft optional (ordering AFTER). Version ranges are filled from gradle/libs.versions.toml at processResources.
Gradle physically keeps Minecraft classes off the pc-sim classpath. ArchUnit (SimArchitectureTest) also guards rules the compiler cannot check:
| Rule | Forbids in ..sim..
|
|---|---|
noMinecraft |
Dependencies on net.minecraft.., com.mojang.., net.neoforged.., dan200.computercraft.., com.simibubi..
|
noUnseededRandom |
Math.random() and new java.util.Random(); only seeded SplittableRandom
|
noWallClock |
System.currentTimeMillis() / nanoTime() outside sim.core.profile
|
threadsOnlyInExec |
new Thread(...) outside sim.core.exec
|
layers |
Physics (sim.neutronics, sim.thermal, sim.thermo, sim.hydraulics, sim.reactor, sim.nuclide) may only access sim.core, and may only be accessed by systems and core |
SimArchitectureRulesTripTest (tag T-05-01) proves the rules actually fire, using deliberate violators in sim.archfixture (LeakyMinecraftUser, UnseededRandomUser, WallClockUser) and a fake net.minecraft.core.BlockPos in the test tree. The layers are declared optional for now, since the physics and systems packages are still empty.
| Package | Module | Contents |
|---|---|---|
sim.core |
pc-sim |
SimulationWorld, SimClock, TimeDomain, SimId, SimCommand, SimEvent, SimSnapshot, CommandQueue, EventBuffer, state writer/reader, migrations |
sim.core.exec, sim.core.profile
|
pc-sim | Worker pool; profiler and JFR events |
sim.reactor, sim.neutronics, sim.thermal, sim.thermo, sim.hydraulics, sim.nuclide
|
pc-sim | Physics |
sim.protection |
pc-sim |
ProtectionSystem, TripReason, TripSource
|
sim.instrument, sim.alarm, sim.acoustics
|
pc-sim | Sensors, actuators, tags, DeviceDescriptor; AlarmEngine; AcousticField
|
sim.radiation, sim.accident, sim.consequence, sim.journal
|
pc-sim | Radiation field, accident models, damage budget, event journal |
sim.chem, sim.enrichment, sim.mechanics, sim.automation
|
pc-sim | Chemistry, cascades, rotating machines, PLC/SIS |
api |
pc-api |
PromptCriticalApi, registration events, SimulationAccess
|
mod.registry, mod.binding, mod.block, mod.blockentity, mod.item
|
pc-mod | Registries, BE ↔ domain adapters, game objects |
mod.net, mod.client
|
pc-mod | Payloads and sync services; BERs, screens, sound |
mod.compat.cc, mod.compat.create, mod.compat.claims
|
pc-mod | CC peripherals; Create kinetics and Ponder; FTB Chunks / OPAC |
mod.config, mod.command, mod.data
|
pc-mod | Config spec, /pc commands, datagen |
TimeDomain and SimClock are the only real simulation code so far:
| Domain | Period | Physical time per step |
|---|---|---|
FAST |
every tick (0.05 s) | 0.05 s (real time) |
SLOW |
every 20 ticks (1 s real) |
K_slow s (time.k_slow, default 72) |
STORAGE |
every 1200 ticks (1 min real) |
K_storage × 60 s (storage.k_storage, default 8766 ≈ one physical year per real hour) |
SimClock keeps a monotonic tick counter (saved with the world) and offers simTime(), slowPhysTime(), slowDtPhys(), storageDtPhys(), slowAlpha() (interpolation between SLOW results for FAST consumers) and due(domain, phase). Phases spread SLOW jobs of different subsystems over different ticks. It is the only time source allowed in sim.
promptcritical-server.toml, built with ModConfigSpec in ServerConfig:
| Key | Default | Range | Meaning |
|---|---|---|---|
time.k_slow |
72.0 | 1–3600 | Physical seconds per real second in SLOW |
storage.k_storage |
8766.0 | 1–10⁶ | Physical seconds per real second in STORAGE |
nuclide.k_burn |
4.0 | 1–100 | Burnup acceleration |
nuclide.decay_heat_clock |
SPLIT |
SPLIT / REAL_TIME
|
Slow decay-heat groups on K_slow, or all real-time (hardcore) |
energy.w_per_su |
1000.0 | 1–10⁶ | Watts of shaft power per Create stress unit |
energy.j_per_fe |
2.5 | 0.01–10⁶ | Joules per Forge Energy unit |
cc.token_rate |
400 | 10–100 000 | CC peripheral call tokens per second |
cc.token_burst |
800 | 10–200 000 | CC token bucket capacity |
server.mode |
CONTAINED |
REALISTIC / CONTAINED / CREATIVE_SAFE
|
Consequence policy |
damage.max_blocks_per_tick |
64 | 0–4096 | World block changes per tick from accidents |
A fixed order is part of the determinism contract:
| # | Stage | Subsystem |
|---|---|---|
| 1 | Apply commands | CommandQueue.drainSorted() |
| 2 | Actuators | Rod and valve travel; scram rod release (flag from stage 10 of the previous tick) |
| 3 | Neutron kinetics | sim.neutronics |
| 4 | Heat structures | sim.thermal |
| 5 | Hydraulics | sim.hydraulics |
| 6 | Machines | Pumps, turbines, cascades (coupled to Create shaft speed) |
| 7 | Accident models | Cladding, H₂, containment |
| 8 | Acoustics | sim.acoustics |
| 9 | Sensors | Measurements from the state after stages 3–7 (PLC/SIS stages 9a–9c are inserted here) |
| 10 | Protection | Scram decision, applied at stage 2 of the next tick |
| 11 | Alarms | sim.alarm |
| 12 | Publish | SnapshotPublisher |
| 13 | Events |
EventBuffer → binding |
The one-tick (50 ms) scram delay is physically justified: real logic and gripper release take 0.1–0.3 s.
Same initial state + command log + external inputs ⇒ bit-identical state after N ticks. This enables replay, tests and multiplayer fairness.
| Non-determinism source | Measure |
|---|---|
HashMap iteration order |
Forbidden in the hot loop; arrays sorted by SimId
|
| Floating point | Strict IEEE 754 by default since Java 17 (JEP 306) |
Math.sin/exp/log/pow intrinsics |
SimMath facade over StrictMath (fdlibm) |
| Randomness |
SplittableRandom seeded from mix(worldSeed, simId, streamId) (ArchUnit-enforced) |
| Parallel SLOW jobs | Merged at a fixed tick regardless of completion time |
| Command order across threads | Sorted on drain |
| Wall clock | Forbidden in sim except the profiler (ArchUnit-enforced) |
SimulationWorld.stateHash() (xxHash64 over all SoA arrays) is exposed as /pc debug hash and used in tests.
Three options were compared: everything on the server thread, a separate pipelined simulation thread, and a hybrid (chosen). A pipelined thread would always see the world one tick late, while block changes and Create kinetics happen on the server thread inside the tick, a source of subtle desyncs. FAST physics is cheap; expensive work is slow by nature and easy to offload.
Tick protocol:
ServerTickEvent.Pre (server thread) workers (perf.worker_threads)
1. TopologyRebuilder.process(DirtySet) (<= 300 us)
2. ExternalInputs.capture() (Create kinetics, chunk loading)
3. for each subsystem due(SLOW, phase):
a) join job started 20 ticks ago --- wait <= perf.slow_join_timeout_ms ---+
b) mergeSlow(result) |
c) submit next job with a copy of inputs ----------------------------> run SLOW job
4. same for STORAGE (period 1200)
5. SimulationWorld.tick(): FAST stages
6. RadiationField: merge finished ray batches, start the next -----------> ray tracing
ServerTickEvent.Post
7. DamageBudget.drain() (<= 64 blocks)
8. CcEventPump: SimEvent -> queueEvent
9. Journal, chat announcements
10. InstrumentSyncService / AcousticSyncService: encode and send
- A SLOW job started on tick t is always merged on tick t + 20, even if it finished earlier, so the state does not depend on worker speed.
- If it is not ready, the server thread waits up to
perf.slow_join_timeout_ms = 5ms and then steals the remaining work. A SLOW step is never skipped. -
perf.worker_threadsdefaults to max(1, min(2, N_cpu − 2)). -
CommandQueueis a bounded MPSC array queue (capacity 4096). Producers are CC threads, the network thread and the server thread. Overflow returnsE_RATEto Lua. On drain, commands are sorted by source class (automation/watchdog/policy/admin = 0, desk = 1, Lua = 2), then source id, then sequence; at most 512 per tick.
Every block entity with a domain twin implements DeviceHost (simId(), level(), pos(), mode(), lazy peripheral()). Lifecycle:
| Minecraft event | Binding action |
|---|---|
BlockEntity.onLoad() (server) |
SimLevelBinding.attach; register if simId is empty |
| Block placed | Mark the neighbourhood dirty for incremental topology rebuild |
| Block broken / exploded |
detach + mark dirty; device removed at the start of the next tick |
| Chunk unload / load | Station load state machine (freeze / thaw) |
| Level unload / server stopping | Save, close pools |
ServerTickEvent.Pre |
Tick protocol above |
Principle: send the client only what the player sees or hears.
| Data | Recipients | Rate |
|---|---|---|
| Desk gauges, displays, lamps, annunciators, mimics | Players within net.instrument_radius = 24 m |
Gauges 10 Hz, digital displays 2 Hz, lamps on event |
| Chart recorder paper | Same | 1 point/s, batches every 5 s |
| Open GUIs (vibrometer, block screens) | The opener | 1 Hz |
| Sound emitters | Within 48 m | 4 Hz deltas |
| Player dose and contamination | Self | 1 Hz |
| CC monitors | — | Synced by CC itself |
Budget: 2 KB/s per player, split into token buckets net.budget.instruments = 1400, net.budget.acoustics = 400, net.budget.player = 100 B/s plus reserve. A typical control room (40 gauges, 10 displays, 4 recorders, 2 annunciators, 20 mimic elements, 20 sound emitters) needs about 1180 B/s, worst case ≈ 2050 B/s. When the instrument bucket runs dry, far gauges (> 12 m) fall back to 5 Hz and then 2 Hz. Lamps and alarms are always sent.
| Level | Tool | Location | Status |
|---|---|---|---|
| Unit | JUnit 5 + AssertJ | pc-sim/src/test |
SimClockTest exists |
| Architecture | ArchUnit | pc-sim/src/test |
Implemented (rules above) |
| Properties | jqwik | pc-sim/src/test |
Engine wired; 1000 tries per commit, 10⁵ nightly planned |
| Physics validation | JUnit + reference tables (IAPWS-IF97, analytic kinetics) | pc-sim/src/test/.../validation |
Planned |
| Determinism | JUnit replay | pc-sim/src/test |
Planned |
| Concurrency | jcstress | pc-sim/src/jcstress |
Planned (snapshot seqlock, command queue) |
| Integration | NeoForge GameTest | pc-mod/src/gametest |
BootstrapGameTests: API installed, server config loaded with defaults, Create loaded |
| Performance | JMH | pc-sim/src/jmh |
Plugin applied; benchmarks planned |
Traceability (planned): every test-case ID in the GDD tables (T-3b-01, T-4a-17, ...) must have a test annotated @Tag("..."). A meta-test GddTraceabilityTest will parse the design document tables and fail if an ID has no test. The existing architecture trip test already carries @Tag("T-05-01").
JMH targets on the reference machine (4 cores, ~3.5 GHz, JDK 21): point kinetics step ≤ 5 µs; one reference core ≤ 150 µs; fluid network of 400 nodes ≤ 500 µs; 400 sensor channels ≤ 10 µs; 500 alarms ≤ 15 µs; 200 acoustic emitters ≤ 10 µs; full tick with 4 reactors and 20 players p95 ≤ 2.0 ms; 0 bytes allocated per FAST operation.
Implemented: PromptCriticalApi.get() returns the instance that the mod installs into ApiHolder during construction. It currently exposes only apiVersion(). ApiHolder.install throws if called twice or with null; get() throws if the API is not initialized yet.
Planned:
-
SimulationAccess simulation(dimension): read-only access for other mods, through the same sensor model as Lua (read(stationId, tag),tags(stationId),subscribeEvents(listener)). - Registration events on the mod bus:
RegisterProcessUnitsEvent,RegisterInstrumentsEvent,RegisterPeripheralExtensionsEvent(cost and call class mandatory),RegisterAccidentHandlersEvent. - Materials, nuclides and fuel come from datapacks, not Java.
- ROM Lua (
pcui,pcctl,pctest, examples) ships through the mod'sdata/computercraft/lua/rom/...datapack.
- Building-from-Source — how to build and run what exists
- Art-Pipeline — models and textures
- CC-Tweaked-API — threading of peripheral calls, snapshots, command queue
- Create-Integration — kinetic ports
- Multiplayer — station loading, server modes, damage budget
- Roadmap — milestones M0–M15
Prompt Critical · © 2026 denfry · Repository · Report an issue
Start here
Design
Nuclear
Industries
Control and automation
World and server
Development