diff --git a/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java b/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java index fdfd486..ceb4b82 100644 --- a/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java +++ b/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java @@ -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 diff --git a/src/main/java/com/mythlane/gravityflip/command/DumpParticlesCommand.java b/src/main/java/com/mythlane/gravityflip/command/DumpParticlesCommand.java new file mode 100644 index 0000000..6d479ea --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/command/DumpParticlesCommand.java @@ -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). + * + *

API used: {@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. + * + *

Trigger: 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. + * + *

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 lineSink) { + DefaultAssetMap 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. Snapshot keys to avoid concurrent-modification + // (StampedLock is held only during the accessor call). + List 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(); + } +}