- Portfolio-ready showcase with YouTube placeholder, screenshots gallery, feature list
- Command reference table for the 6 subcommands
- Inventory of 12 bundled showcase regions with coordinates and purpose
- Install / development / license sections
- docs/screenshots/.gitkeep to reserve asset location
- Reduce javadocs to one-liners across config/region/physics/tick/viz/plugin root
- Translate residual French comments; no behavioural change
- Tests adjusted where assertions referenced French strings
- Removed a comment regarding the rejection of quoted arguments with spaces, as the validation logic is already covered by existing tests. This helps to keep the test code clean and focused.
Root cause: Item.Interactions map value codec is RootInteraction.CHILD_ASSET_CODEC
(expects a String asset id referencing an asset under Item/RootInteractions/), not
an inline Interaction object. Previous JSON used "Primary": { "Type": "GravityFlipWand" }
which silently loaded a RootInteraction asset with no interactionIds -> operations[]
empty -> InteractionManager.serverTick NPE on operation.getWaitForDataFrom() when
the click chain ticked.
Fix:
- New Interaction asset Server/Item/Interactions/gravityflip_wand_click.json
({"Type": "GravityFlipWand"}) matching our registered Interaction subtype.
- New RootInteraction asset Server/Item/RootInteractions/gravityflip_wand_root.json
wrapping the interaction id in its Interactions[] list (required by RootInteraction
CODEC, see RootInteraction.build() -> OperationsBuilder).
- Item JSON now references the RootInteraction by id:
"Interactions": { "Primary": "gravityflip_wand_root", "Secondary": "gravityflip_wand_root" }
matching vanilla Weapon_Wand_Wood.json pattern ("Primary": "Wand_Primary").
Binding (Interaction.CODEC.register("GravityFlipWand", ...)) is unchanged and
correct -- subtype registration on the Interaction AssetCodecMapCodec, consistent
with InstancesPlugin/ExitInstanceInteraction pattern.
Empirical findings from HytaleServer.jar decompile + Assets.zip introspection:
- AssetRegistryLoader.loadAssets0() resolves each plugin asset-pack root to
<root>/Server/, then iterates AssetStores and loads from
Server/<assetStore.getPath()>/. For the Item AssetStore, path is
'Item/Items'. So the wand JSON MUST live at Server/Item/Items/.
- The previous src/main/resources/Items/gravityflip_wand.json path was
never scanned: the loader would look at <jar>/Server/Item/Items/.
- Key function is Item::getId with item.id = blockTypeKey (filename minus
.json). So file 'gravityflip_wand.json' -> lookup id 'gravityflip_wand'.
JSON content cloned from vanilla Weapon_Wand_Root.json (extracted from
HYTALE SERVER/Assets.zip), which is the minimal wand template:
- Categories: ['Items.Weapons'] -> shows up in builder/creative menu.
- Icon/Model/Texture reuse vanilla wand assets (no custom art shipped).
- PlayerAnimationsId: Wand -> proper holding animation.
- ItemSoundSetId: ISS_Weapons_Wand -> pickup/drop SFX.
- Utility.Compatible=true and Weapon={} keep it functional in builder tools.
- Interactions.Primary/Secondary are inline {Type: GravityFlipWand} objects,
which the plugin's CodecRegistry(Interaction.CODEC).register call binds
to GravityFlipWandInteraction.CODEC (04-01 pattern, Finding 3).
Without this flag, JavaPlugin.setup0() skips AssetModule.registerPack(),
so bundled Items/*.json are never loaded into Item.getAssetMap(), causing
/gravityflip wand to fail with 'Item gravityflip_wand introuvable'.
- AbstractPlayerCommand (tp only makes sense for a player caller)
- Pattern mirrors TeleportToCoordinatesCommand:48-68 (read Transform+HeadRotation,
build Teleport.createForPlayer(pos, bodyRotation).setHeadRotation(headRotation),
store.addComponent)
- Center computed componentwise: c = (min+max)/2
- Preserves caller body+head rotation (teleport in place, no look change)
- Clear error on unknown region name
- GravityFlipListSubCommand: iterate registry.all(), format 'name : (min) -> (max) [state]'
- GravityFlipDeleteSubCommand: registry.remove + save, clear error on unknown name
- GravityFlipToggleSubCommand: read current enabled, flip via setEnabled, save, report new state
- All three extend CommandBase (works from server console, not just player)
- Save-failure path: keep in-memory change, truthful message (mirror define pattern)
- GravityFlipWandInteraction extends SimpleInstantInteraction; Primary/Secondary
routed through firstRun(), writes to WandSelectionStore via volatile bindStore()
- Items/gravityflip_wand.json: Utility item (Icon Torch_Fire) whose Primary and
Secondary Interactions reference Type=GravityFlipWand
- GravityFlipPlugin.setup(): constructs WandSelectionStore, injects it into the
interaction class, then registers via getCodecRegistry(Interaction.CODEC)
(pattern from InstancesPlugin:158 / ExitInstanceInteraction)
- Expose wandSelections() getter for Phase 04-02+ commands
Documentation-only class capturing the 04-00 spike conclusions. Not wired
into GravityFlipPlugin. Private constructor prevents instantiation.
Records the locked VANILLA_COPY decision:
- Do NOT register a new Item asset store (unproven pattern).
- Subclass SimpleInstantInteraction and register via
getCodecRegistry(Interaction.CODEC).register("GravityFlipWand", ...).
- Plan 04-02 will runtime-identify a vanilla hatchet/axe itemID for
/gravityflip wand to hand out.
Delete this class at the start of Plan 04-01.
- Implemented a mechanism to bootstrap a demo gravity flip region when regions.json is absent or empty.
- The showcase region features a 10×20×10 box with upward force and Torch_Fire particles, allowing immediate demonstration of functionality.
- Users can customize or remove this region via regions.json.
Log cleanup:
- Drop redundant INFO "Gravity Flip enabled" in setup() (substantive version at
start() remains as the single startup line).
- Drop INFO "debug enter/exit notifier ENABLED" (the notifier itself emits per
region transition; extra startup noise not needed).
- Remove dead back-compat GravityApplier ctors that accepted an infoHandler
Consumer (no callers post-03-06; debug logs already stripped).
- Clean stale javadoc referencing removed [DBG npc.woken]/[DBG npc.ctrlNull]
one-shot logs.
- Also ship the pre-staged cleanup work: RegionEnterNotifier gated behind
GRAVITYFLIP_DEBUG_NOTIFY env var, GravityApplier infoHandler field removed,
tick logs stripped.
Efficiency:
- RegionVisualizer.resolveParticleId now memoises the requested->resolved
mapping in a ConcurrentHashMap, eliminating the ParticleSystem.getAssetMap()
lookup on every tick emission per region. Warn-once semantics preserved via
warnedInvalidIds. Fail-open path (AssetMap unavailable in tests) intentionally
does not populate the cache.
- Document in GravityApplier javadoc why Pass 1 + Pass 2 cannot be fused:
restore requires the complete currentlyInRegion set before diffing against
previouslyInverted.
Considered but not applied:
- ParticleEdgeEmitter.edgePoints caching per (box, density): throttled to
>=100ms refresh and typical <20 regions => alloc pressure negligible;
premature without measurement.
- Reusing RegionRegistry snapshot in RegionEnterNotifier: notifier is
opt-in/off-by-default, so its independent ECS scan has zero prod cost.
Tests: ./gradlew clean build green, no test changes required.
- New DumpParticlesCommand utility: enumerates loaded ParticleSystem
asset-ids via ParticleSystem.getAssetMap().getAssetMap().keySet().
- Wired in GravityFlipPlugin.start() behind GRAVITYFLIP_DUMP_PARTICLES
env var (or -Dgravityflip.dumpParticles sysprop). No-op when unset.
- Boot-time fallback approach (plan-allowed) -- proper CommandBase
registration deferred to avoid i18n translation-key plumbing for a
one-shot discovery dump.
- See .planning/phases/03-gravity-physics/03-06-DUMP-NOTES.md for
trigger instructions and the user checkpoint before Task 3.
- RegionTickLoop: nouveau ctor 4-arg (registry, gravityApplier, regionVisualizer,
errorHandler). Les ctors 2-arg et 3-arg existants delegent (back-compat tests).
Tick appelle regionVisualizer.visualize(w, snapshot) apres gravityApplier.apply()
avec null-safe gate (meme pattern que gravityApplier).
- GravityFlipPlugin.start(): construit RegionVisualizer avec errorHandler log WARN,
le passe au tickLoop.
- GravityFlipPlugin.shutdown(): appelle regionVisualizer.clearAll(world) AVANT
tickLoop.stop() (si Universe encore vivante), emet ClearDebugShapes a tous
les PlayerRefs. Si universe null, fallback TTL expiration.
- Aucun nouveau scheduler/thread cree.
Ajoute une tâche Gradle `copyJarToDevServer` finalisant shadowJar :
le fat JAR est copié dans C:/Users/minit/Desktop/HYTALE SERVER/Server/mods
par défaut. Overridable via -PdevServerMods=... et désactivable avec
-PdevServerMods=disabled.
- RegionRegistry.refreshFor dispatche scan ECS + publishSnapshot via world.execute
- Publication devient asynchrone (1-tick staleness max, borne par tick loop @100ms)
- Null-path enabled.isEmpty publie directement (pas de travail ECS requis)
- Amendement D-04 du plan 03-01 : world.execute requis côté initiation de forEachEntityParallel
- GravityApplier.apply dispatche via world.execute(Runnable) pour satisfaire assertThread de Store.forEachEntityParallel
- PASS 1, PASS 2 et update tracker exécutés dans le même Runnable (cohérence intra-tick)
- Null-guards conservés avant le dispatch
- Amendement D-04 du plan 03-01 : world.execute requis côté initiation
- RegionTickLoop gagne un constructeur 3-args (registry, gravityApplier, errorHandler)
- Ancien constructeur 2-args conservé pour rétrocompat des tests Phase 02
- Tick body appelle gravityApplier.apply(w, registry.currentSnapshot(w)) après refreshFor
- GravityFlipPlugin.start() construit l'applier et l'injecte dans le tickLoop
- Log de démarrage mis à jour: 'gravity inversion active'
- Service GravityApplier qui toggle PhysicsValues.invertedGravity via CommandBuffer.replaceComponent
- Thread-safe tracker ConcurrentHashMap.newKeySet<UUID> pour dedup entre ticks
- Second pass O(N) pour restaurer la gravité à la sortie de région (trade-off v1 documenté)
- Pure diff static helper + hooks package-private pour tests unitaires sans runtime Hytale
- 6 tests verts (diff + tracker semantics)
- RegionTickLoop: single-thread daemon scheduler @100ms, 2s initial delay,
shutdown -> awaitTermination(5s) -> shutdownNow lifecycle.
Three start overloads: (World), (Supplier<World>), (Runnable). Errors are
routed to a Consumer<Throwable> so an exception in one tick never kills
the scheduler.
- GravityFlipPlugin.start():
* builds RegionRegistry from configHolder.get() + holder for save()
* starts the tick loop with a Supplier<World> that lazily resolves
Universe.get().getDefaultWorld() (deviation: PrepareUniverseEvent in the
pinned API only carries WorldConfigProvider — no Universe/World access,
so we use the lazy-supplier pattern from MythWorld instead)
* registers the ScheduledFuture with TaskRegistry (raw cast, try/catch fallback)
- GravityFlipPlugin.shutdown(): tickLoop.stop() BEFORE super.shutdown()
- RegionTickLoopTest: 3 timing tests pass (>=8 ticks/sec, stop within 5s,
exception resilience).
- gradle build green; fat jar contains both region/ and tick/ class dirs.
- GravityFlipConfig wraps List<GravityFlipRegion> via ArrayCodec under the
KeyedCodec("Regions", ...) entry. Decoded list is a mutable ArrayList
(NOT List.of(arr)) so Phase 4 commands can mutate at runtime.
- GravityFlipPlugin.configHolder uses the named overload
withConfig("regions", GravityFlipConfig.CODEC) — produces
<dataDirectory>/regions.json. The 1-arg overload would hardcode config.json.
- configHolder() javadoc documents the save contract (no auto-save on
shutdown; mutators must call configHolder.save() explicitly) and the shared
mutable reference caveat that 02-02's RegionRegistry will resolve.
- 4 round-trip tests cover: empty default, two-region order preservation,
empty list, and list mutability (regression guard against List.of).
- GravityFlipRegion holds Name (String) + Box (Hytale AABB) + Enabled (boolean),
serialised through a BuilderCodec keyed on "Name"/"Box"/"Enabled".
- Validators.nonNull() applied to Name and Box (correct package is
com.hypixel.hytale.codec.validation.Validators, not codec.builder).
- Server jar 2026.03.26 uses com.hypixel.hytale.math.vector.Vector3d for Box.min/max
(NOT org.joml.Vector3d, which only appeared in newer builds).
- 3 JUnit 5 round-trip tests cover name/box/enabled, enabled=false, and empty name.
- testImplementation added for the Server artifact so codec encode/decode is
reachable from unit tests.