feat(03-06): add particle-system dump for discovery (Task 1)

- 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.
This commit is contained in:
2026-04-23 15:32:55 +02:00
parent 3511493687
commit 7bbd65dad2
2 changed files with 122 additions and 0 deletions
@@ -5,6 +5,7 @@ import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.server.core.universe.Universe;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.util.Config;
import com.mythlane.gravityflip.command.DumpParticlesCommand;
import com.mythlane.gravityflip.config.GravityFlipConfig;
import com.mythlane.gravityflip.physics.FallDamageGuard;
import com.mythlane.gravityflip.physics.FallDamageSuppressorSystem;
@@ -103,6 +104,22 @@ public class GravityFlipPlugin extends JavaPlugin {
getLogger().at(Level.INFO).log(
"Gravity Flip enabled — %d region(s) loaded, detector @100ms, gravity inversion active",
cfg.getRegions().size());
// Plan 03-06 Task 1 — one-shot ParticleSystem asset-id dump for default
// discovery. Enabled via env var GRAVITYFLIP_DUMP_PARTICLES=1 (or sysprop
// -Dgravityflip.dumpParticles=true). No-op otherwise. See
// DumpParticlesCommand javadoc for the chosen API rationale.
if (DumpParticlesCommand.isEnabled()) {
try {
int n = DumpParticlesCommand.dump(line ->
getLogger().at(Level.INFO).log("%s", line));
getLogger().at(Level.INFO).log(
"ParticleSystem dump complete: %d asset-ids logged", n);
} catch (Throwable th) {
getLogger().at(Level.WARNING).withCause(th)
.log("ParticleSystem dump failed");
}
}
}
@Override
@@ -0,0 +1,105 @@
package com.mythlane.gravityflip.command;
import com.hypixel.hytale.assetstore.map.DefaultAssetMap;
import com.hypixel.hytale.server.core.asset.type.particle.config.ParticleSystem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.logging.Logger;
/**
* Discovery utility — dumps all loaded {@link ParticleSystem} asset-ids to the
* plugin logger. Used during Phase 3 plan 06 to pick a default
* {@code VisualParticleId} from the 561 particle systems shipped with the base
* Hytale asset pack (only two string-literals are visible in the decompiled
* source, so a runtime dump is the cleanest discovery path).
*
* <p><b>API used:</b> {@code ParticleSystem.getAssetMap().getAssetMap().keySet()}
* — static accessor resolved via {@link com.hypixel.hytale.assetstore.AssetRegistry}.
* See decompiled {@code ParticleSystem#getAssetStore()} and
* {@code DefaultAssetMap#getAssetMap()}; the underlying map uses a
* case-insensitive hash strategy.
*
* <p><b>Trigger:</b> set the environment variable
* {@code GRAVITYFLIP_DUMP_PARTICLES=1} (or {@code =true}) before launching the
* server. A proper {@code /gf dumpparticles} command can replace this later —
* the boot-time fallback is explicitly allowed by 03-06-PLAN.md Task 1.
*
* <p>This class intentionally does not extend
* {@code com.hypixel.hytale.server.core.command.system.basecommands.CommandBase}
* because the full command-registration path requires i18n translation keys
* ({@code Message.translation(...)}) for description and per-argument help,
* which is out-of-scope for a throwaway discovery task. Name "Command" is kept
* to satisfy the plan artifact path.
*/
public final class DumpParticlesCommand {
/** Env var (preferred, zero-config). */
public static final String ENV_VAR = "GRAVITYFLIP_DUMP_PARTICLES";
/** System property fallback (e.g. {@code -Dgravityflip.dumpParticles=true}). */
public static final String SYS_PROP = "gravityflip.dumpParticles";
private static final String LOG_PREFIX = "[dumpparticles]";
private DumpParticlesCommand() {}
/**
* @return true if either {@link #ENV_VAR} or {@link #SYS_PROP} is set to a
* truthy value ({@code "1"}, {@code "true"}, {@code "yes"}, case-insensitive).
*/
public static boolean isEnabled() {
return isTruthy(System.getenv(ENV_VAR)) || isTruthy(System.getProperty(SYS_PROP));
}
private static boolean isTruthy(String v) {
if (v == null) return false;
String s = v.trim().toLowerCase();
return s.equals("1") || s.equals("true") || s.equals("yes") || s.equals("on");
}
/**
* Dump all loaded ParticleSystem asset-ids via the supplied logger, one id
* per line prefixed with {@value #LOG_PREFIX}. Safe to call from
* {@code JavaPlugin.start()} (post-LoadAssetEvent). If the asset store has
* not yet populated (unlikely at start(), but defensive), logs a warning
* and returns zero.
*
* @return number of ids emitted (>= 0)
*/
public static int dump(Logger logger) {
return dump(line -> logger.info(line));
}
/**
* Testable variant — accepts any line consumer (logger.info, println, list::add).
*/
public static int dump(Consumer<String> lineSink) {
DefaultAssetMap<String, ParticleSystem> map;
try {
map = ParticleSystem.getAssetMap();
} catch (Throwable th) {
lineSink.accept(LOG_PREFIX + " ERROR: ParticleSystem asset store not available: " + th);
return 0;
}
if (map == null) {
lineSink.accept(LOG_PREFIX + " ERROR: ParticleSystem.getAssetMap() returned null");
return 0;
}
// DefaultAssetMap#getAssetMap() returns an unmodifiable view of the
// underlying Map<K, T>. Snapshot keys to avoid concurrent-modification
// (StampedLock is held only during the accessor call).
List<String> ids = new ArrayList<>(map.getAssetMap().keySet());
Collections.sort(ids);
lineSink.accept(LOG_PREFIX + " BEGIN — " + ids.size() + " ParticleSystem asset-ids loaded:");
for (String id : ids) {
lineSink.accept(LOG_PREFIX + " " + id);
}
lineSink.accept(LOG_PREFIX + " END");
return ids.size();
}
}