From 2033df265506936a7aaa71837015e8e63981bcff Mon Sep 17 00:00:00 2001 From: RedSavant Date: Wed, 21 Jan 2026 14:59:11 +0100 Subject: [PATCH] Initial commit --- .gitignore | 27 + README.md | 99 +++ app/build.gradle.kts | 29 + .../main/java/org/example/ExamplePlugin.java | 33 + app/src/main/java/org/example/MyConfig.java | 31 + .../docs/commands/ArgumentTypesExample.java | 595 ++++++++++++++++++ .../docs/commands/CommandContextExample.java | 399 ++++++++++++ .../commands/CreatingCommandsExample.java | 162 +++++ .../docs/commands/SubcommandsExample.java | 331 ++++++++++ .../docs/coreconcepts/AssetsExample.java | 68 ++ .../docs/coreconcepts/CodecsExample.java | 160 +++++ .../docs/coreconcepts/RegistriesExample.java | 84 +++ .../docs/coreconcepts/ThreadingExample.java | 168 +++++ .../docs/effects/DynamicLightsExample.java | 217 +++++++ .../docs/effects/EffectsIndexExample.java | 81 +++ .../docs/effects/EntityEffectsExample.java | 318 ++++++++++ .../docs/effects/ParticlesExample.java | 144 +++++ .../docs/entities/EntitiesIndexExample.java | 49 ++ .../entities/EntityComponentsExample.java | 175 ++++++ .../docs/entities/EntityHierarchyExample.java | 199 ++++++ .../docs/entities/PlayerApiExample.java | 164 +++++ .../docs/events/AsyncEventsExample.java | 320 ++++++++++ .../docs/events/EventSystemExample.java | 48 ++ .../gettingstarted/CreatingPluginExample.java | 56 ++ .../PluginLifecycleExample.java | 91 +++ .../InteractionsIndexExample.java | 51 ++ .../docs/interactions/SelectorsExample.java | 88 +++ .../docs/inventory/ContainersExample.java | 499 +++++++++++++++ .../docs/inventory/InventoryExample.java | 212 +++++++ .../docs/inventory/ItemStacksExample.java | 289 +++++++++ .../docs/inventory/TransactionsExample.java | 362 +++++++++++ .../docs/permissions/PermissionsExample.java | 301 +++++++++ .../docs/reference/AllRegistriesExample.java | 69 ++ .../org/example/docs/tasks/TasksExample.java | 244 +++++++ .../example/docs/ui/CustomPagesExample.java | 208 ++++++ .../example/docs/ui/HudManagerExample.java | 149 +++++ .../org/example/docs/ui/UiIndexExample.java | 66 ++ .../org/example/docs/ui/WindowsExample.java | 83 +++ .../org/example/docs/world/BlocksExample.java | 197 ++++++ .../org/example/docs/world/ChunksExample.java | 156 +++++ .../example/docs/world/PlayerRefsExample.java | 117 ++++ .../docs/world/UniverseWorldsExample.java | 106 ++++ app/src/main/resources/manifest.json | 18 + .../java/org/example/ExamplePluginTest.java | 12 + build.gradle.kts | 6 + gradle.properties | 5 + gradle/libs.versions.toml | 10 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45633 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 ++++++++ gradlew.bat | 93 +++ settings.gradle.kts | 7 + setup.sh | 140 +++++ 53 files changed, 7791 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/build.gradle.kts create mode 100644 app/src/main/java/org/example/ExamplePlugin.java create mode 100644 app/src/main/java/org/example/MyConfig.java create mode 100644 app/src/main/java/org/example/docs/commands/ArgumentTypesExample.java create mode 100644 app/src/main/java/org/example/docs/commands/CommandContextExample.java create mode 100644 app/src/main/java/org/example/docs/commands/CreatingCommandsExample.java create mode 100644 app/src/main/java/org/example/docs/commands/SubcommandsExample.java create mode 100644 app/src/main/java/org/example/docs/coreconcepts/AssetsExample.java create mode 100644 app/src/main/java/org/example/docs/coreconcepts/CodecsExample.java create mode 100644 app/src/main/java/org/example/docs/coreconcepts/RegistriesExample.java create mode 100644 app/src/main/java/org/example/docs/coreconcepts/ThreadingExample.java create mode 100644 app/src/main/java/org/example/docs/effects/DynamicLightsExample.java create mode 100644 app/src/main/java/org/example/docs/effects/EffectsIndexExample.java create mode 100644 app/src/main/java/org/example/docs/effects/EntityEffectsExample.java create mode 100644 app/src/main/java/org/example/docs/effects/ParticlesExample.java create mode 100644 app/src/main/java/org/example/docs/entities/EntitiesIndexExample.java create mode 100644 app/src/main/java/org/example/docs/entities/EntityComponentsExample.java create mode 100644 app/src/main/java/org/example/docs/entities/EntityHierarchyExample.java create mode 100644 app/src/main/java/org/example/docs/entities/PlayerApiExample.java create mode 100644 app/src/main/java/org/example/docs/events/AsyncEventsExample.java create mode 100644 app/src/main/java/org/example/docs/events/EventSystemExample.java create mode 100644 app/src/main/java/org/example/docs/gettingstarted/CreatingPluginExample.java create mode 100644 app/src/main/java/org/example/docs/gettingstarted/PluginLifecycleExample.java create mode 100644 app/src/main/java/org/example/docs/interactions/InteractionsIndexExample.java create mode 100644 app/src/main/java/org/example/docs/interactions/SelectorsExample.java create mode 100644 app/src/main/java/org/example/docs/inventory/ContainersExample.java create mode 100644 app/src/main/java/org/example/docs/inventory/InventoryExample.java create mode 100644 app/src/main/java/org/example/docs/inventory/ItemStacksExample.java create mode 100644 app/src/main/java/org/example/docs/inventory/TransactionsExample.java create mode 100644 app/src/main/java/org/example/docs/permissions/PermissionsExample.java create mode 100644 app/src/main/java/org/example/docs/reference/AllRegistriesExample.java create mode 100644 app/src/main/java/org/example/docs/tasks/TasksExample.java create mode 100644 app/src/main/java/org/example/docs/ui/CustomPagesExample.java create mode 100644 app/src/main/java/org/example/docs/ui/HudManagerExample.java create mode 100644 app/src/main/java/org/example/docs/ui/UiIndexExample.java create mode 100644 app/src/main/java/org/example/docs/ui/WindowsExample.java create mode 100644 app/src/main/java/org/example/docs/world/BlocksExample.java create mode 100644 app/src/main/java/org/example/docs/world/ChunksExample.java create mode 100644 app/src/main/java/org/example/docs/world/PlayerRefsExample.java create mode 100644 app/src/main/java/org/example/docs/world/UniverseWorldsExample.java create mode 100644 app/src/main/resources/manifest.json create mode 100644 app/src/test/java/org/example/ExamplePluginTest.java create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 setup.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c9516d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Gradle +.gradle/ +build/ +app/build/ + +# IDE +.idea/ +*.iml +.vscode/ +*.swp +.classpath +.project +.settings/ + +# OS +.DS_Store +Thumbs.db + +# Hytale +*.jar +!gradle/wrapper/gradle-wrapper.jar +.hytale-downloader-credentials.json + +# Setup +.bin/ +server/ +src-ref/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a47b236 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Example Hytale Plugin + +This is an example Hytale server plugin demonstrating the basic structure and setup. + +## Project Structure + +``` +example-mod/ +├── app/ +│ ├── src/ +│ │ ├── main/ +│ │ │ ├── java/org/example/ +│ │ │ │ └── ExamplePlugin.java +│ │ │ └── resources/ +│ │ │ └── plugin.json +│ │ └── test/ +│ ├── build.gradle.kts +├── gradle/ +├── server/ +├── build.gradle.kts +├── settings.gradle.kts +└── gradlew / gradlew.bat +``` + +## Setup + +### Pre-setup +```bash +# Download the hytale-downloader & cfr (DO IT ONCE) +./setup --download +``` + +### Setup +```bash +# Download the HytaleServer, finally located here ./server/HytaleServer.jar +./setup --setup +``` + +### Decompile +```bash +# Decompile ./server/HytaleServer.jar to get the source, finally located here ./src-ref +./setup --decompile +``` + + + +## Building + +```bash +# Build the plugin +./gradlew build + +# The JAR will be in app/build/libs/ExamplePlugin-1.0.0.jar +``` + +## Installation + +1. Build the plugin with `./gradlew build` +2. Copy `app/build/libs/ExamplePlugin-1.0.0.jar` to your Hytale server's `mods/` folder +3. Start the server + +## Plugin Structure + +### manifest.json + +The plugin manifest file defines metadata about your plugin: + +```json +{ + "Group": "org.example", + "Name": "ExamplePlugin", + "Version": "1.0.0", + "Description": "An example Hytale server plugin", + "Authors": [ + { + "Name": "Your Name" + } + ], + "Main": "org.example.ExamplePlugin", + "ServerVersion": "*", + "Dependencies": {}, + "OptionalDependencies": {}, + "DisabledByDefault": false, + "IncludesAssetPack": false, + "SubPlugins": [] +} +``` + +### Main Plugin Class + +Your plugin class extends `JavaPlugin` and implements lifecycle methods: + +- `setup()` - Called during server setup, register configs here +- `start()` - Called when plugin starts, register commands/events here +- `shutdown()` - Called when plugin stops, cleanup resources here + +## Requirements + +- Java 25 or later diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..75305d1 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + java +} + +group = "org.example" +version = "1.0.0" + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +dependencies { + // Hytale Server API - compile only since it's provided at runtime + compileOnly(files("../server/Server/HytaleServer.jar")) + + // Testing + testImplementation(libs.junit) +} + +tasks.jar { + // Set the archive name + archiveBaseName.set("ExamplePlugin") +} diff --git a/app/src/main/java/org/example/ExamplePlugin.java b/app/src/main/java/org/example/ExamplePlugin.java new file mode 100644 index 0000000..f8b22dc --- /dev/null +++ b/app/src/main/java/org/example/ExamplePlugin.java @@ -0,0 +1,33 @@ +package org.example; + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.util.Config; + +/** + * Main example plugin. + * Individual documentation examples are in org.example.docs package. + */ +public class ExamplePlugin extends JavaPlugin { + private Config config; + + public ExamplePlugin(JavaPluginInit init) { + super(init); + config = withConfig(MyConfig.CODEC); + } + + @Override + public void setup() { + + } + + @Override + public void start() { + // Register commands from documentation examples + getCommandRegistry().registerCommand(new org.example.docs.commands.CreatingCommandsExample.HelloCommand()); + } + + @Override + public void shutdown() { + } +} diff --git a/app/src/main/java/org/example/MyConfig.java b/app/src/main/java/org/example/MyConfig.java new file mode 100644 index 0000000..0fe7cd6 --- /dev/null +++ b/app/src/main/java/org/example/MyConfig.java @@ -0,0 +1,31 @@ +package org.example; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; + +public class MyConfig { + private String serverName; + private int maxPlayers = 20; // Default value + private boolean debugMode = false; + + public static final BuilderCodec CODEC = BuilderCodec.builder(MyConfig.class, MyConfig::new) + .append(new KeyedCodec<>("ServerName", Codec.STRING), + (config, val) -> config.serverName = val, + config -> config.serverName) + .add() + .append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER), + (config, val) -> config.maxPlayers = val, + config -> config.maxPlayers) + .add() + .append(new KeyedCodec<>("DebugMode", Codec.BOOLEAN), + (config, val) -> config.debugMode = val, + config -> config.debugMode) + .add() + .build(); + + // Getters... + public String getServerName() { return serverName; } + public int getMaxPlayers() { return maxPlayers; } + public boolean isDebugMode() { return debugMode; } +} diff --git a/app/src/main/java/org/example/docs/commands/ArgumentTypesExample.java b/app/src/main/java/org/example/docs/commands/ArgumentTypesExample.java new file mode 100644 index 0000000..9eb9314 --- /dev/null +++ b/app/src/main/java/org/example/docs/commands/ArgumentTypesExample.java @@ -0,0 +1,595 @@ +package org.example.docs.commands; + +// Example from: hytale-docs/content/commands/argument-types.en.md +// This file tests that ALL argument-types documentation examples compile correctly + +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.asset.type.entityeffect.config.EntityEffect; +import com.hypixel.hytale.server.core.asset.type.particle.config.ParticleSystem; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.protocol.GameMode; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.math.vector.Vector3i; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.core.command.system.arguments.types.RelativeDoublePosition; +import com.hypixel.hytale.server.core.command.system.arguments.types.RelativeIntPosition; +import com.hypixel.hytale.server.core.command.system.arguments.types.Coord; +import java.util.UUID; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Tests argument-types.en.md documentation + * + * IMPORTANT CORRECTIONS FOUND: + * - Documentation uses Component.text() which doesn't exist - use Message.raw() instead + * - Documentation uses playerRef.getPlayer() which doesn't exist - use getReference() for ECS + * - Documentation uses playerRef.getWorld() which doesn't exist - use getWorldUuid() + * - Documentation should use context.sendMessage(Message) instead of context.sender().sendMessage() + */ +public class ArgumentTypesExample { + + // ============================================ + // From "Primitive Types" section - Examples tab + // ============================================ + public static class MathCommand extends AbstractCommand { + private final RequiredArg countArg; + private final RequiredArg multiplierArg; + private final RequiredArg verboseArg; + private final OptionalArg labelArg; + + public MathCommand() { + super("math", "Perform mathematical operations"); + countArg = withRequiredArg("count", "Number of iterations", ArgTypes.INTEGER); + multiplierArg = withRequiredArg("multiplier", "Multiplication factor", ArgTypes.DOUBLE); + verboseArg = withRequiredArg("verbose", "Show detailed output", ArgTypes.BOOLEAN); + labelArg = withOptionalArg("label", "Optional label", ArgTypes.STRING); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + int count = context.get(countArg); + double mult = context.get(multiplierArg); + boolean verbose = context.get(verboseArg); + String label = context.get(labelArg); // null if not provided + + double result = count * mult; + + if (verbose) { + String output = (label != null ? label + ": " : "") + + count + " × " + mult + " = " + result; + // CORRECTION: Use context.sendMessage(Message.raw()) instead of context.sender().sendMessage() + context.sendMessage(Message.raw(output)); + } + + return null; + } + } + + // ============================================ + // From "Player and Entity Types" - PlayerRef tab + // CORRECTION: playerRef.getPlayer() doesn't exist + // ============================================ + public static class TeleportCommand extends AbstractCommand { + private final RequiredArg targetArg; + private final OptionalArg destinationArg; + + public TeleportCommand() { + super("tp", "Teleport players"); + targetArg = withRequiredArg("target", "Player to teleport", ArgTypes.PLAYER_REF); + destinationArg = withOptionalArg("destination", "Destination player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef targetRef = context.get(targetArg); + + // CORRECTION: PlayerRef doesn't have getPlayer() method + // Instead use getReference() to get Ref + // For now, we use username which IS available + String targetUsername = targetRef.getUsername(); + + if (context.provided(destinationArg)) { + PlayerRef destRef = context.get(destinationArg); + String destUsername = destRef.getUsername(); + context.sendMessage(Message.raw("Would teleport " + targetUsername + " to " + destUsername)); + } + + return null; + } + } + + // ============================================ + // From "Player and Entity Types" - Entity tab + // ============================================ + public static class EntityInfoCommand extends AbstractCommand { + private final RequiredArg entityArg; + + public EntityInfoCommand() { + super("entityinfo", "Get entity information"); + entityArg = withRequiredArg("entity", "Entity UUID", ArgTypes.UUID); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + UUID entityId = context.get(entityArg); + + // Find entity in worlds + for (World world : Universe.get().getWorlds().values()) { + // Search for entity by UUID + // Entity lookup depends on world API + } + + context.sendMessage(Message.raw("Entity lookup complete")); + return null; + } + } + + // ============================================ + // From "Player and Entity Types" - UUID tab + // ============================================ + public static class UuidCommand extends AbstractCommand { + private final RequiredArg uuidArg; + + public UuidCommand() { + super("uuid", "Parse and display UUID"); + uuidArg = withRequiredArg("uuid", "UUID to parse", ArgTypes.UUID); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + UUID uuid = context.get(uuidArg); + context.sendMessage(Message.raw( + "UUID: " + uuid.toString() + "\nVersion: " + uuid.version() + )); + return null; + } + } + + // ============================================ + // From "World Types" section + // CORRECTION: Uses Component.text() which doesn't exist + // ============================================ + public static class WorldTeleportCommand extends AbstractCommand { + private final RequiredArg worldArg; + private final OptionalArg positionArg; + + public WorldTeleportCommand() { + super("wtp", "Teleport to another world"); + worldArg = withRequiredArg("world", "Destination world", ArgTypes.WORLD); + positionArg = withOptionalArg("position", "Spawn position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // CORRECTION: Use context.sender() instanceof Player, then cast + if (!(context.sender() instanceof Player player)) { + // CORRECTION: Use Message.raw() not Component.text() + context.sendMessage(Message.raw("Only players can use this command!")); + return null; + } + + World targetWorld = context.get(worldArg); + + // Note: World.getSpawnPosition() and player.teleport() would need verification + // For now just show the concept works + context.sendMessage(Message.raw("Would teleport to world: " + targetWorld.getName())); + + return null; + } + } + + // ============================================ + // From "Asset Types" - Items tab + // CORRECTION: Uses Component.text() and ItemStack.of() which may not exist + // ============================================ + public static class GiveCommand extends AbstractCommand { + private final RequiredArg itemArg; + private final OptionalArg amountArg; + private final OptionalArg targetArg; + + public GiveCommand() { + super("give", "Give items to players"); + itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET); + amountArg = withOptionalArg("amount", "Stack size", ArgTypes.INTEGER); + targetArg = withOptionalArg("player", "Target player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + Item item = context.get(itemArg); + int amount = context.provided(amountArg) ? context.get(amountArg) : 1; + + // CORRECTION: playerRef.getPlayer() doesn't exist + // We can only get username from PlayerRef + String targetName; + if (context.provided(targetArg)) { + targetName = context.get(targetArg).getUsername(); + } else if (context.sender() instanceof Player p) { + targetName = p.getDisplayName(); + } else { + context.sendMessage(Message.raw("Specify a player!")); + return null; + } + + // CORRECTION: Use Message.raw() instead of Component.text() + context.sendMessage( + Message.raw("Gave " + amount + "x " + item.getId() + " to " + targetName) + ); + + return null; + } + } + + // ============================================ + // From "Asset Types" - Blocks tab + // ============================================ + public static class SetBlockCommand extends AbstractCommand { + private final RequiredArg blockArg; + private final RequiredArg positionArg; + + public SetBlockCommand() { + super("setblock", "Set a block at position"); + blockArg = withRequiredArg("block", "Block type", ArgTypes.BLOCK_TYPE_ASSET); + positionArg = withRequiredArg("position", "Target position", ArgTypes.RELATIVE_BLOCK_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + BlockType blockType = context.get(blockArg); + // Note: player.getBlockPosition() would need verification + // RelativeIntPosition.resolve() takes a Vector3i + + context.sendMessage( + Message.raw("Set block to " + blockType.getId()) + ); + + return null; + } + } + + // ============================================ + // From "Asset Types" - Effects tab + // ============================================ + public static class EffectCommand extends AbstractCommand { + private final RequiredArg effectArg; + private final OptionalArg durationArg; + private final OptionalArg amplifierArg; + + public EffectCommand() { + super("effect", "Apply effect to player"); + effectArg = withRequiredArg("effect", "Effect to apply", ArgTypes.EFFECT_ASSET); + durationArg = withOptionalArg("duration", "Duration in ticks", ArgTypes.INTEGER); + amplifierArg = withOptionalArg("amplifier", "Effect level", ArgTypes.INTEGER); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + EntityEffect effect = context.get(effectArg); + int duration = context.provided(durationArg) ? context.get(durationArg) : 600; + int amplifier = context.provided(amplifierArg) ? context.get(amplifierArg) : 0; + + // Note: player.addEffect() would need verification + context.sendMessage(Message.raw("Applied effect: " + effect.getId())); + + return null; + } + } + + // ============================================ + // From "Asset Types" - Particles tab + // ============================================ + public static class ParticleCommand extends AbstractCommand { + private final RequiredArg particleArg; + private final OptionalArg positionArg; + + public ParticleCommand() { + super("particle", "Spawn particle effect"); + particleArg = withRequiredArg("particle", "Particle system", ArgTypes.PARTICLE_SYSTEM); + positionArg = withOptionalArg("position", "Spawn position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + ParticleSystem particle = context.get(particleArg); + // Note: Would need to verify World.spawnParticle() exists + + context.sendMessage(Message.raw("Spawned particle: " + particle.getId())); + return null; + } + } + + // ============================================ + // From "Position Types" - 3D Positions tab + // ============================================ + public static class TeleportPosCommand extends AbstractCommand { + private final RequiredArg posArg; + + public TeleportPosCommand() { + super("tppos", "Teleport to coordinates"); + posArg = withRequiredArg("position", "Target position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + RelativeDoublePosition relPos = context.get(posArg); + + // Note: Need to verify player.getPosition() and relPos.resolve() APIs + // player.teleport(absolutePos) would also need verification + + context.sendMessage(Message.raw("Teleported to position")); + return null; + } + } + + // ============================================ + // From "Position Types" - Block Positions tab + // ============================================ + public static class FillCommand extends AbstractCommand { + private final RequiredArg pos1Arg; + private final RequiredArg pos2Arg; + private final RequiredArg blockArg; + + public FillCommand() { + super("fill", "Fill region with blocks"); + pos1Arg = withRequiredArg("from", "First corner", ArgTypes.RELATIVE_BLOCK_POSITION); + pos2Arg = withRequiredArg("to", "Second corner", ArgTypes.RELATIVE_BLOCK_POSITION); + blockArg = withRequiredArg("block", "Block type", ArgTypes.BLOCK_TYPE_ASSET); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + // RelativeIntPosition values + RelativeIntPosition from = context.get(pos1Arg); + RelativeIntPosition to = context.get(pos2Arg); + BlockType block = context.get(blockArg); + + // Note: Need to verify these APIs exist + // player.getBlockPosition() returns Vector3i? + // world.setBlock(Vector3i, BlockType)? + + context.sendMessage(Message.raw("Filled region with " + block.getId())); + return null; + } + } + + // ============================================ + // From "Position Types" - Single Coords tab + // ============================================ + public static class SetYCommand extends AbstractCommand { + private final RequiredArg yArg; + + public SetYCommand() { + super("sety", "Teleport to Y level"); + yArg = withRequiredArg("y", "Y coordinate", ArgTypes.RELATIVE_DOUBLE_COORD); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + Coord relY = context.get(yArg); + // Note: relY.resolve(double) API needs verification + + context.sendMessage(Message.raw("Set Y level")); + return null; + } + } + + // ============================================ + // From "Vector Types" - Examples tab + // ============================================ + public static class LookCommand extends AbstractCommand { + private final RequiredArg rotationArg; + + public LookCommand() { + super("look", "Set player rotation"); + rotationArg = withRequiredArg("rotation", "Pitch Yaw Roll", ArgTypes.ROTATION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + Vector3f rotation = context.get(rotationArg); + // rotation.x = pitch, rotation.y = yaw, rotation.z = roll + // Note: player.setRotation() API needs verification + + // CORRECTION: Vector3f uses public fields, not methods + context.sendMessage(Message.raw( + "Rotation set to Pitch: " + rotation.x + + ", Yaw: " + rotation.y + + ", Roll: " + rotation.z + )); + + return null; + } + } + + // ============================================ + // From "Special Types" - Color tab + // ============================================ + public static class ColorCommand extends AbstractCommand { + private final RequiredArg colorArg; + + public ColorCommand() { + super("color", "Set display color"); + colorArg = withRequiredArg("color", "Color value", ArgTypes.COLOR); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + int color = context.get(colorArg); + + int alpha = (color >> 24) & 0xFF; + int red = (color >> 16) & 0xFF; + int green = (color >> 8) & 0xFF; + int blue = color & 0xFF; + + context.sendMessage( + Message.raw("Color: ARGB(" + alpha + ", " + red + ", " + green + ", " + blue + ")") + ); + + return null; + } + } + + // ============================================ + // From "Special Types" - Game Mode tab + // ============================================ + public static class GameModeCommand extends AbstractCommand { + private final RequiredArg modeArg; + private final OptionalArg playerArg; + + public GameModeCommand() { + super("gamemode", "Change game mode"); + modeArg = withRequiredArg("mode", "Game mode", ArgTypes.GAME_MODE); + playerArg = withOptionalArg("player", "Target player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + GameMode mode = context.get(modeArg); + + String targetName; + if (context.provided(playerArg)) { + PlayerRef ref = context.get(playerArg); + targetName = ref.getUsername(); + } else if (context.sender() instanceof Player p) { + targetName = p.getDisplayName(); + } else { + context.sendMessage(Message.raw("Specify a player!")); + return null; + } + + // Set game mode using ChangeGameModeEvent or component + context.sendMessage(Message.raw("Game mode changed to " + mode.name() + " for " + targetName)); + + return null; + } + } + + // ============================================ + // From "Special Types" - Tick Rate tab + // ============================================ + public static class TickRateCommand extends AbstractCommand { + private final RequiredArg rateArg; + + public TickRateCommand() { + super("tickrate", "Set world tick rate"); + rateArg = withRequiredArg("rate", "Tick rate", ArgTypes.TICK_RATE); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + int tps = context.get(rateArg); // Returns Integer (TPS) + // Set tick rate on world... + + context.sendMessage(Message.raw("Tick rate set to " + tps + " TPS")); + + return null; + } + } + + // ============================================ + // From "Custom Argument Types" - Enum Arguments section + // ============================================ + public enum Difficulty { + EASY, NORMAL, HARD, EXTREME + } + + public static class DifficultyCommand extends AbstractCommand { + private final RequiredArg difficultyArg; + + public DifficultyCommand() { + super("difficulty", "Set server difficulty"); + difficultyArg = withRequiredArg( + "level", + "Difficulty level", + ArgTypes.forEnum("Difficulty", Difficulty.class) + ); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + Difficulty level = context.get(difficultyArg); + // Apply difficulty setting + context.sendMessage( + Message.raw("Difficulty set to " + level.name()) + ); + return null; + } + } + + // ============================================ + // From "Custom Argument Types" - List Arguments section + // ============================================ + public static class MultiTeleportCommand extends AbstractCommand { + private final RequiredArg> playersArg; + private final RequiredArg positionArg; + + public MultiTeleportCommand() { + super("multitp", "Teleport multiple players"); + playersArg = withListRequiredArg("players", "Target players", ArgTypes.PLAYER_REF); + positionArg = withRequiredArg("position", "Destination", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + List playerRefs = context.get(playersArg); + RelativeDoublePosition relPos = context.get(positionArg); + + // Note: playerRef.getPlayer() doesn't exist, use getReference() + for (PlayerRef ref : playerRefs) { + // Would need to use ECS to teleport + String username = ref.getUsername(); + } + + context.sendMessage( + Message.raw("Teleported " + playerRefs.size() + " players") + ); + + return null; + } + } + + // Note: Argument Validation section - .validate() method usage would need verification +} diff --git a/app/src/main/java/org/example/docs/commands/CommandContextExample.java b/app/src/main/java/org/example/docs/commands/CommandContextExample.java new file mode 100644 index 0000000..083d7a2 --- /dev/null +++ b/app/src/main/java/org/example/docs/commands/CommandContextExample.java @@ -0,0 +1,399 @@ +package org.example.docs.commands; + +// Example from: hytale-docs/content/commands/command-context.en.md +// This file tests that ALL command-context documentation examples compile correctly + +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.CommandSender; +import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.system.DefaultArg; +import com.hypixel.hytale.server.core.command.system.arguments.system.FlagArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.exceptions.GeneralCommandException; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import java.util.concurrent.CompletableFuture; + +/** + * Tests command-context.en.md documentation + * + * IMPORTANT CORRECTIONS FOUND: + * - Line 46: context.sender().sendMessage("text") should use Message.raw() + * - Line 189: playerRef.getWorld() doesn't exist - use getWorldUuid() + * - Line 201: playerRef.sendMessage() exists, but Message.of() doesn't - use Message.raw() + */ +public class CommandContextExample { + + // ============================================ + // From "Accessing the Context" section + // ============================================ + public static class BasicContextCommand extends AbstractCommand { + + public BasicContextCommand() { + super("basiccontext", "Shows basic context usage"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Use context here + return null; + } + } + + // ============================================ + // From "Command Sender" section + // ============================================ + public static class SenderCommand extends AbstractCommand { + + public SenderCommand() { + super("sender", "Demonstrates sender access"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + CommandSender sender = context.sender(); + + // Send messages (must use Message class) + sender.sendMessage(Message.raw("Hello!")); + sender.sendMessage(Message.translation("my.translation.key")); + + // Check permissions + if (sender.hasPermission("myplugin.admin")) { + // ... + } + + return null; + } + } + + // ============================================ + // From "Checking Sender Type" section + // CORRECTION: sendMessage needs Message.raw() + // ============================================ + public static class SenderTypeCommand extends AbstractCommand { + + public SenderTypeCommand() { + super("sendertype", "Checks sender type"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (context.sender() instanceof Player) { + Player player = (Player) context.sender(); + // Player-specific logic + } else { + // Console or other sender + // CORRECTION: Use Message.raw() instead of raw String + context.sender().sendMessage(Message.raw("This command requires a player!")); + } + return null; + } + } + + // ============================================ + // From "Getting Arguments" - Required Arguments + // ============================================ + public static class RequiredArgsCommand extends AbstractCommand { + private final RequiredArg playerArg; + private final RequiredArg countArg; + + public RequiredArgsCommand() { + super("requiredargs", "Required arguments example"); + playerArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF); + countArg = withRequiredArg("count", "Count value", ArgTypes.INTEGER); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // In execute(): + PlayerRef player = context.get(playerArg); // Never null for required args + int count = context.get(countArg); // Never null for required args + return null; + } + } + + // ============================================ + // From "Getting Arguments" - Optional Arguments + // ============================================ + public static class OptionalArgsCommand extends AbstractCommand { + private final OptionalArg reasonArg; + + public OptionalArgsCommand() { + super("optionalargs", "Optional arguments example"); + reasonArg = withOptionalArg("reason", "Optional reason", ArgTypes.STRING); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Get value (may be null if not provided) + String reason = context.get(reasonArg); + + // Check if provided before using + if (context.provided(reasonArg)) { + String providedReason = context.get(reasonArg); + } + return null; + } + } + + // ============================================ + // From "Getting Arguments" - Default Arguments + // ============================================ + public static class DefaultArgsCommand extends AbstractCommand { + private final DefaultArg countArg; // Default: 1 + + public DefaultArgsCommand() { + super("defaultargs", "Default arguments example"); + countArg = withDefaultArg("count", "Number", ArgTypes.INTEGER, 1, "1"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Always returns a value (never null) + int count = context.get(countArg); // Returns default if not specified + return null; + } + } + + // ============================================ + // From "Getting Arguments" - Flag Arguments + // ============================================ + public static class FlagArgsCommand extends AbstractCommand { + private final FlagArg silentFlag; + + public FlagArgsCommand() { + super("flagargs", "Flag arguments example"); + silentFlag = withFlagArg("silent", "Silent mode"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Check if flag was provided + boolean isSilent = context.provided(silentFlag); + return null; + } + } + + // ============================================ + // From "Input String" section + // ============================================ + public static class InputStringCommand extends AbstractCommand { + + public InputStringCommand() { + super("inputstring", "Shows input string access"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + String input = context.getInputString(); + // For "/give player123 sword 5" -> "give player123 sword 5" + return null; + } + } + + // ============================================ + // From "The Command" section + // ============================================ + public static class CommandInfoCommand extends AbstractCommand { + + public CommandInfoCommand() { + super("commandinfo", "Shows command info access"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + AbstractCommand command = context.getCalledCommand(); + String commandName = command.getName(); + String fullName = command.getFullyQualifiedName(); // e.g., "admin kick" + return null; + } + } + + // ============================================ + // From "PLAYER_REF Fallback" section + // CORRECTION: PlayerRef doesn't have getPlayer() or getWorld() + // ============================================ + public static class PlayerRefFallbackCommand extends AbstractCommand { + private final OptionalArg targetArg; + + public PlayerRefFallbackCommand() { + super("playerreffallback", "PlayerRef fallback example"); + targetArg = withOptionalArg("target", "Target player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef target = context.get(targetArg); + + // Manual fallback if null + // CORRECTION: Use getPlayerRef() from Player (though deprecated) + if (target == null && context.sender() instanceof Player player) { + target = player.getPlayerRef(); // Note: getPlayerRef() is deprecated + } + // ... + return null; + } + } + + // ============================================ + // From "World Argument Fallback" section + // ============================================ + public static class WorldFallbackCommand extends AbstractCommand { + private final OptionalArg worldArg; + + public WorldFallbackCommand() { + super("worldfallback", "World fallback example"); + worldArg = withOptionalArg("world", "Target world", ArgTypes.WORLD); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + World world = context.get(worldArg); + + // Manual fallback if null + if (world == null && context.sender() instanceof Player player) { + world = player.getWorld(); // Note: Need to verify this exists + } + // ... + return null; + } + } + + // ============================================ + // From "Error Handling" section + // CORRECTION: PlayerRef doesn't have getPlayer() + // ============================================ + public static class ErrorHandlingCommand extends AbstractCommand { + private final RequiredArg playerArg; + + public ErrorHandlingCommand() { + super("errorhandling", "Error handling example"); + playerArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef playerRef = context.get(playerArg); + + // CORRECTION: PlayerRef doesn't have getPlayer() - use ECS pattern + // Instead, we can check if the reference is valid + Ref ref = playerRef.getReference(); + if (ref == null || !ref.isValid()) { + throw new GeneralCommandException( + Message.translation("error.player.offline") + .param("player", playerRef.getUsername()) + ); + } + + // Continue execution + return null; + } + } + + // ============================================ + // From "Asynchronous Commands" section + // CORRECTION: playerRef.getWorld() doesn't exist + // ============================================ + public static class AsyncCommand extends AbstractCommand { + + public AsyncCommand() { + super("asynccmd", "Async command example"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + PlayerRef playerRef = player.getPlayerRef(); // Note: deprecated + // CORRECTION: Use getWorldUuid() instead of getWorld() + // Or use player.getWorld() directly + World world = player.getWorld(); + + return CompletableFuture.runAsync(() -> { + // Async operation (e.g., database query) + // PlayerData data = database.loadData(playerRef.getUuid()); + + // Return to world thread for game logic + world.execute(() -> { + Ref ref = playerRef.getReference(); + if (ref != null) { + // applyData(ref, data); + playerRef.sendMessage(Message.raw("Data loaded!")); + } + }); + }); + } + } + + // ============================================ + // From "Complete Example" section + // CORRECTION: playerRef.getPlayer() doesn't exist + // ============================================ + public static class GiveCommand extends AbstractCommand { + + private final RequiredArg itemArg; + private final OptionalArg targetArg; + private final DefaultArg countArg; + private final FlagArg silentFlag; + + public GiveCommand() { + super("give", "Give items to a player"); + + itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET); + targetArg = withOptionalArg("target", "Target player", ArgTypes.PLAYER_REF); + countArg = withDefaultArg("count", "Amount", ArgTypes.INTEGER, 1, "1"); + silentFlag = withFlagArg("silent", "Don't broadcast"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Get arguments + Item item = context.get(itemArg); + int count = context.get(countArg); // Uses default if not specified + boolean silent = context.provided(silentFlag); + + // Get target with fallback to sender + PlayerRef targetRef = context.get(targetArg); + if (targetRef == null && context.sender() instanceof Player senderPlayer) { + targetRef = senderPlayer.getPlayerRef(); // Note: deprecated + } + + if (targetRef == null) { + throw new GeneralCommandException( + Message.raw("Must specify a target player!") + ); + } + + // Validate target is online + // CORRECTION: PlayerRef doesn't have getPlayer() + // Use getReference() to check if valid + Ref ref = targetRef.getReference(); + if (ref == null || !ref.isValid()) { + throw new GeneralCommandException( + Message.raw("Player is not online!") + ); + } + + // Execute - give item to player + // ... (would use ECS/inventory APIs) + + // Feedback + if (!silent) { + context.sender().sendMessage(Message.raw( + "Gave " + count + "x " + item.getId() + " to " + targetRef.getUsername() + )); + } + + return null; + } + } +} diff --git a/app/src/main/java/org/example/docs/commands/CreatingCommandsExample.java b/app/src/main/java/org/example/docs/commands/CreatingCommandsExample.java new file mode 100644 index 0000000..0c5d95e --- /dev/null +++ b/app/src/main/java/org/example/docs/commands/CreatingCommandsExample.java @@ -0,0 +1,162 @@ +package org.example.docs.commands; + +// Example from: hytale-docs/content/commands/creating-commands.en.md +// This file tests that ALL commands documentation examples compile correctly + +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.system.DefaultArg; +import com.hypixel.hytale.server.core.command.system.arguments.system.FlagArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +// For ECS operations: import com.hypixel.hytale.component.Ref; +// For ECS operations: import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.Message; +import java.util.concurrent.CompletableFuture; + +/** + * Tests creating-commands.en.md documentation + */ +public class CreatingCommandsExample { + + // From "Basic Command Structure" section + public static class HelloCommand extends AbstractCommand { + + public HelloCommand() { + super("hello", "Sends a greeting message"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + context.sendMessage(Message.raw("Hello, World!")); + return null; + } + } + + // From "Command with Arguments" section + public static class GiveCommand extends AbstractCommand { + + private final RequiredArg targetArg; + private final RequiredArg itemArg; + + public GiveCommand() { + super("give", "Give an item to a player"); + + targetArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF); + itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef targetRef = context.get(targetArg); + Item item = context.get(itemArg); + + // PlayerRef provides direct access to player info + String username = targetRef.getUsername(); + + // For ECS operations, use getReference() to access the EntityStore + // Ref entityRef = targetRef.getReference(); + + // Give item to player... + context.sendMessage(Message.raw("Gave item to " + username)); + + return null; + } + } + + // From "Adding Aliases" section + public static class TeleportCommand extends AbstractCommand { + + public TeleportCommand() { + super("teleport", "Teleport to a location"); + addAliases("tp", "warp"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } + + // From "Optional Arguments" section + public static class GiveWithOptionalCommand extends AbstractCommand { + + private final RequiredArg targetArg; + private final RequiredArg itemArg; + private final OptionalArg countArg; + + public GiveWithOptionalCommand() { + super("giveopt", "Give items to a player"); + + targetArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF); + itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET); + countArg = withOptionalArg("count", "Number of items", ArgTypes.INTEGER); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef targetRef = context.get(targetArg); + Item item = context.get(itemArg); + Integer count = context.get(countArg); // May be null + + int amount = count != null ? count : 1; + // Give items to player... + + return null; + } + } + + // From "Default Arguments" section + public static class GiveWithDefaultCommand extends AbstractCommand { + + private final DefaultArg countArg; + + public GiveWithDefaultCommand() { + super("givedef", "Give items to a player"); + + countArg = withDefaultArg("count", "Number of items", + ArgTypes.INTEGER, 1, "defaults to 1"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + int count = context.get(countArg); // Never null, uses default + return null; + } + } + + // From "Flag Arguments" section + public static class BroadcastCommand extends AbstractCommand { + + private final FlagArg silentFlag; + + public BroadcastCommand() { + super("broadcast", "Send a message to all players"); + + silentFlag = withFlagArg("silent", "Don't show sender name"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + boolean silent = context.provided(silentFlag); + return null; + } + } + + // From "Requiring Confirmation" section + public static class ResetCommand extends AbstractCommand { + + public ResetCommand() { + super("reset", "Reset all player data", true); // requiresConfirmation = true + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // This only runs if --confirm was provided + return null; + } + } +} diff --git a/app/src/main/java/org/example/docs/commands/SubcommandsExample.java b/app/src/main/java/org/example/docs/commands/SubcommandsExample.java new file mode 100644 index 0000000..dc39f12 --- /dev/null +++ b/app/src/main/java/org/example/docs/commands/SubcommandsExample.java @@ -0,0 +1,331 @@ +package org.example.docs.commands; + +// Example from: hytale-docs/content/commands/subcommands.en.md +// This file tests that ALL subcommands documentation examples compile correctly + +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; +import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import java.util.concurrent.CompletableFuture; + +/** + * Tests subcommands.en.md documentation + * + * IMPORTANT CORRECTIONS FOUND: + * - playerRef.getPlayer() doesn't exist - PlayerRef only provides username, UUID, and getReference() + * - Player.kick(String) may not exist with that signature - needs verification + */ +public class SubcommandsExample { + + // ============================================ + // From "Creating Subcommands" - Basic Structure + // ============================================ + public static class AdminCommand extends AbstractCommand { + + public AdminCommand() { + super("admin", "Administration commands"); + + // Add subcommands + addSubCommand(new KickSubCommand()); + addSubCommand(new BanSubCommand()); + addSubCommand(new MuteSubCommand()); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // This runs when no subcommand is specified + context.sender().sendMessage(Message.raw("Usage: /admin ")); + return null; + } + } + + // ============================================ + // From "Subcommand Implementation" + // CORRECTION: playerRef.getPlayer() doesn't exist + // ============================================ + public static class KickSubCommand extends AbstractCommand { + + private final RequiredArg playerArg; + private final OptionalArg reasonArg; + + public KickSubCommand() { + super("kick", "Kick a player from the server"); + + playerArg = withRequiredArg("player", "Player to kick", ArgTypes.PLAYER_REF); + reasonArg = withOptionalArg("reason", "Kick reason", ArgTypes.STRING); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef target = context.get(playerArg); + String reason = context.get(reasonArg); + + // CORRECTION: PlayerRef doesn't have getPlayer() method + // We can only verify the player is online via getReference() + Ref ref = target.getReference(); + if (ref != null && ref.isValid()) { + // To kick, would need to access the PacketHandler or use events + // target.getPacketHandler().disconnect(reason); + String kickReason = reason != null ? reason : "Kicked by admin"; + context.sender().sendMessage(Message.raw("Kicked " + target.getUsername() + ": " + kickReason)); + } else { + context.sender().sendMessage(Message.raw("Player " + target.getUsername() + " is not online")); + } + + return null; + } + } + + // Placeholder subcommands for AdminCommand + public static class BanSubCommand extends AbstractCommand { + private final RequiredArg playerArg; + + public BanSubCommand() { + super("ban", "Ban a player from the server"); + playerArg = withRequiredArg("player", "Player to ban", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef target = context.get(playerArg); + context.sender().sendMessage(Message.raw("Banned " + target.getUsername())); + return null; + } + } + + public static class MuteSubCommand extends AbstractCommand { + private final RequiredArg playerArg; + + public MuteSubCommand() { + super("mute", "Mute a player"); + playerArg = withRequiredArg("player", "Player to mute", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef target = context.get(playerArg); + context.sender().sendMessage(Message.raw("Muted " + target.getUsername())); + return null; + } + } + + // ============================================ + // From "Command Collections" section + // ============================================ + public static class ManageCommand extends AbstractCommandCollection { + + public ManageCommand() { + super("manage", "Management commands"); + + addSubCommand(new ManageUsersCommand()); + addSubCommand(new ManageWorldsCommand()); + addSubCommand(new ManagePluginsCommand()); + } + } + + // ============================================ + // From "Nested Subcommands" section + // ============================================ + public static class ManageUsersCommand extends AbstractCommand { + + public ManageUsersCommand() { + super("users", "User management"); + + addSubCommand(new ListUsersCommand()); // /manage users list + addSubCommand(new AddUserCommand()); // /manage users add + addSubCommand(new RemoveUserCommand()); // /manage users remove + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Show usage for /manage users + context.sender().sendMessage(Message.raw("Usage: /manage users ")); + return null; + } + } + + // Nested subcommand stubs + public static class ListUsersCommand extends AbstractCommand { + public ListUsersCommand() { + super("list", "List all users"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } + + public static class AddUserCommand extends AbstractCommand { + public AddUserCommand() { + super("add", "Add a user"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } + + public static class RemoveUserCommand extends AbstractCommand { + public RemoveUserCommand() { + super("remove", "Remove a user"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } + + // Placeholder manage subcommands + public static class ManageWorldsCommand extends AbstractCommand { + public ManageWorldsCommand() { + super("worlds", "World management"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } + + public static class ManagePluginsCommand extends AbstractCommand { + public ManagePluginsCommand() { + super("plugins", "Plugin management"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } + + // ============================================ + // From "Subcommand Aliases" section + // ============================================ + public static class TeleportCommand extends AbstractCommand { + + public TeleportCommand() { + super("teleport", "Teleport commands"); + addAliases("tp"); + + addSubCommand(new TeleportHereCommand()); + addSubCommand(new TeleportAllCommand()); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } + + public static class TeleportHereCommand extends AbstractCommand { + private final RequiredArg playerArg; + + public TeleportHereCommand() { + super("here", "Teleport player to you"); + addAliases("h", "tome"); + playerArg = withRequiredArg("player", "Player to teleport", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef target = context.get(playerArg); + context.sender().sendMessage(Message.raw("Teleporting " + target.getUsername() + " to you")); + return null; + } + } + + public static class TeleportAllCommand extends AbstractCommand { + public TeleportAllCommand() { + super("all", "Teleport all players"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } + + // ============================================ + // From "Command Variants" section + // ============================================ + public static class TpCommand extends AbstractCommand { + + private final RequiredArg targetArg; + + public TpCommand() { + super("tp", "Teleport command"); + + // Main variant: /tp + targetArg = withRequiredArg("target", "Player to teleport to", ArgTypes.PLAYER_REF); + + // Add variant: /tp + addUsageVariant(new TpToPlayerVariant()); + + // Note: Position variant would need different argument types + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Teleport sender to target player + PlayerRef target = context.get(targetArg); + context.sender().sendMessage(Message.raw("Teleporting to " + target.getUsername())); + return null; + } + } + + // ============================================ + // From "Variant Implementation" section + // ============================================ + public static class TpToPlayerVariant extends AbstractCommand { + + private final RequiredArg playerArg; + private final RequiredArg destinationArg; + + public TpToPlayerVariant() { + // Note: Documentation says "no name for variants - use description only" + // But AbstractCommand requires name, so we pass empty or description + super("Teleport one player to another"); + + playerArg = withRequiredArg("player", "Player to teleport", ArgTypes.PLAYER_REF); + destinationArg = withRequiredArg("destination", "Destination player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef player = context.get(playerArg); + PlayerRef destination = context.get(destinationArg); + // Teleport player to destination + context.sender().sendMessage(Message.raw("Teleporting " + player.getUsername() + " to " + destination.getUsername())); + return null; + } + } + + // ============================================ + // From "Permission Inheritance" section + // ============================================ + public static class KickWithPermissionCommand extends AbstractCommand { + + public KickWithPermissionCommand() { + super("kick", "Kick a player"); + // Custom permission instead of auto-generated + // Note: requirePermission() method needs verification + // requirePermission("myplugin.admin.kick"); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + return null; + } + } +} diff --git a/app/src/main/java/org/example/docs/coreconcepts/AssetsExample.java b/app/src/main/java/org/example/docs/coreconcepts/AssetsExample.java new file mode 100644 index 0000000..6348646 --- /dev/null +++ b/app/src/main/java/org/example/docs/coreconcepts/AssetsExample.java @@ -0,0 +1,68 @@ +package org.example.docs.coreconcepts; + +// Example from: hytale-docs/content/core-concepts/assets.en.md +// This file tests that ALL assets documentation examples compile correctly + +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.logger.HytaleLogger; +import java.util.logging.Level; + +/** + * Tests ALL assets.en.md documentation examples + */ +public class AssetsExample { + + // ============================================ + // From "Working with Items" section (lines 27-37) + // ============================================ + public void getItemExample(HytaleLogger logger) { + // Get an item by identifier + Item sword = Item.getAssetMap().getAsset("hytale:iron_sword"); + + // Check if item exists + if (sword != null) { + logger.at(Level.INFO).log("Found item: " + sword.getId()); + } + } + + // ============================================ + // From "Creating ItemStacks" section (lines 41-54) + // ============================================ + public void createItemStackExample() { + // Create an ItemStack with quantity + ItemStack stack = new ItemStack("hytale:iron_sword", 5); + + // Or with just the item ID (quantity defaults to 1) + ItemStack singleItem = new ItemStack("hytale:iron_sword"); + + // Access properties + String itemId = stack.getItemId(); + int quantity = stack.getQuantity(); + } + + // ============================================ + // From "Getting a BlockType" section (lines 60-63) + // ============================================ + public void getBlockTypeExample() { + BlockType stone = BlockType.getAssetMap().getAsset("hytale:stone"); + } + + // ============================================ + // From "Block in World" section (lines 67-75) + // Note: Requires Player and World context + // ============================================ + public void blockInWorldExample(Player player) { + int x = 0, y = 0, z = 0; + + // Get block at position + World world = player.getWorld(); + BlockType blockType = world.getBlockType(x, y, z); + + // Set block at position + world.setBlock(x, y, z, "hytale:stone"); + } +} diff --git a/app/src/main/java/org/example/docs/coreconcepts/CodecsExample.java b/app/src/main/java/org/example/docs/coreconcepts/CodecsExample.java new file mode 100644 index 0000000..7c5cdaa --- /dev/null +++ b/app/src/main/java/org/example/docs/coreconcepts/CodecsExample.java @@ -0,0 +1,160 @@ +package org.example.docs.coreconcepts; + +// Example from: hytale-docs/content/core-concepts/codecs.en.md +// This file tests that ALL codecs documentation examples compile correctly + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.array.ArrayCodec; +import com.hypixel.hytale.codec.codecs.map.ObjectMapCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.codec.validation.Validators; +import com.hypixel.hytale.server.core.util.Config; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Level; + +/** + * Tests ALL codecs.en.md documentation examples + */ +public class CodecsExample { + + // ============================================ + // From "Basic Codecs" section - testing all primitive codecs + // ============================================ + public void testBasicCodecs() { + Codec stringCodec = Codec.STRING; + Codec intCodec = Codec.INTEGER; + Codec longCodec = Codec.LONG; + Codec floatCodec = Codec.FLOAT; + Codec doubleCodec = Codec.DOUBLE; + Codec boolCodec = Codec.BOOLEAN; + Codec byteCodec = Codec.BYTE; + Codec shortCodec = Codec.SHORT; + } + + // ============================================ + // From "List and Map Codecs" section + // ============================================ + public void testCollectionCodecs() { + // Array of strings + Codec stringArray = new ArrayCodec<>(Codec.STRING, String[]::new); + + // Map with string keys + Codec> stringIntMap = new ObjectMapCodec<>( + Codec.INTEGER, + LinkedHashMap::new, + key -> key, // Key to string + str -> str // String to key + ); + } + + // ============================================ + // From "Enum Codecs" section + // ============================================ + public enum GameMode { + SURVIVAL, CREATIVE, ADVENTURE + } + + public void testEnumCodec() { + Codec gameModeCodec = new EnumCodec<>(GameMode.class); + } + + // ============================================ + // From "Required vs Optional Fields" section + // ============================================ + public static class ConfigWithValidator { + private String serverName; + private int maxPlayers = 20; + + public static final BuilderCodec CODEC = BuilderCodec.builder(ConfigWithValidator.class, ConfigWithValidator::new) + .append(new KeyedCodec<>("ServerName", Codec.STRING), + (config, val) -> config.serverName = val, + config -> config.serverName) + .addValidator(Validators.nonNull()) // Makes field required + .add() + .append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER), + (config, val) -> config.maxPlayers = val, + config -> config.maxPlayers) + .add() // Optional - uses default value from class + .build(); + } + + // ============================================ + // From "Nested Codecs" section + // ============================================ + public static class GeneralSettings { + private String name; + + public static final BuilderCodec CODEC = BuilderCodec.builder(GeneralSettings.class, GeneralSettings::new) + .append(new KeyedCodec<>("Name", Codec.STRING), + (s, val) -> s.name = val, + s -> s.name) + .add() + .build(); + } + + public static class DatabaseSettings { + private String url; + + public static final BuilderCodec CODEC = BuilderCodec.builder(DatabaseSettings.class, DatabaseSettings::new) + .append(new KeyedCodec<>("Url", Codec.STRING), + (s, val) -> s.url = val, + s -> s.url) + .add() + .build(); + } + + public static class ServerSettings { + private GeneralSettings general; + private DatabaseSettings database; + + public static final BuilderCodec CODEC = BuilderCodec.builder(ServerSettings.class, ServerSettings::new) + .append(new KeyedCodec<>("General", GeneralSettings.CODEC), + (s, val) -> s.general = val, + s -> s.general) + .add() + .append(new KeyedCodec<>("Database", DatabaseSettings.CODEC), + (s, val) -> s.database = val, + s -> s.database) + .add() + .build(); + } + + // ============================================ + // From "Inheritance" section + // ============================================ + public static class BaseEntity { + protected String id; + + public BaseEntity() {} + + public static final BuilderCodec BASE_CODEC = BuilderCodec.builder(BaseEntity.class, BaseEntity::new) + .append(new KeyedCodec<>("Id", Codec.STRING), + (e, val) -> e.id = val, + e -> e.id) + .add() + .build(); + } + + public static class PlayerEntity extends BaseEntity { + private String username; + + public PlayerEntity() { super(); } + + public static final BuilderCodec CODEC = BuilderCodec.builder(PlayerEntity.class, PlayerEntity::new, BaseEntity.BASE_CODEC) + .append(new KeyedCodec<>("Username", Codec.STRING), + (e, val) -> e.username = val, + e -> e.username) + .add() + .build(); + } + + // ============================================ + // From "Named Configs" section - pattern only (requires plugin context) + // ============================================ + // In a plugin class: + // Config mainConfig = withConfig("main", MyConfig.CODEC); + // Config dbConfig = withConfig("database", DatabaseConfig.CODEC); +} diff --git a/app/src/main/java/org/example/docs/coreconcepts/RegistriesExample.java b/app/src/main/java/org/example/docs/coreconcepts/RegistriesExample.java new file mode 100644 index 0000000..8966efc --- /dev/null +++ b/app/src/main/java/org/example/docs/coreconcepts/RegistriesExample.java @@ -0,0 +1,84 @@ +package org.example.docs.coreconcepts; + +// Example from: hytale-docs/content/core-concepts/registries.en.md +// This file tests that ALL registries documentation examples compile correctly + +import com.hypixel.hytale.event.EventPriority; +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.server.core.command.system.CommandRegistry; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.task.TaskRegistry; +import com.hypixel.hytale.logger.HytaleLogger; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +/** + * Tests ALL registries.en.md documentation examples + */ +public class RegistriesExample { + + // ============================================ + // From "EventRegistry" section (lines 43-65) + // ============================================ + public void registerEvents(EventRegistry eventRegistry, HytaleLogger logger) { + // Simple registration + eventRegistry.register(PlayerConnectEvent.class, event -> { + logger.at(Level.INFO).log("Player connecting: " + event.getPlayerRef().getUsername()); + }); + + // With priority + eventRegistry.register( + EventPriority.EARLY, + PlayerConnectEvent.class, + this::onPlayerConnect + ); + } + + private void onPlayerConnect(PlayerConnectEvent event) { + // handler + } + + // ============================================ + // From "CommandRegistry" section (lines 30-38) + // Note: Documentation says register() but should be registerCommand() + // ============================================ + public void registerCommands(CommandRegistry commandRegistry) { + // getCommandRegistry().registerCommand(new MyCommand()); + // Note: Cannot test without actual command class + } + + // ============================================ + // From "TaskRegistry" section (lines 94-129) + // ============================================ + public void registerTasks(TaskRegistry taskRegistry, HytaleLogger logger) { + // Async operation + CompletableFuture task = CompletableFuture.runAsync(() -> { + // Background work + }); + taskRegistry.registerTask(task); + + // Delayed operation (3 seconds) + CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS) + .execute(() -> { + logger.at(Level.INFO).log("Delayed task executed!"); + }); + + // Repeating operation (every 5 minutes) + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + ScheduledFuture repeating = scheduler.scheduleAtFixedRate(() -> { + logger.at(Level.INFO).log("Repeating task!"); + }, 0, 5, TimeUnit.MINUTES); + taskRegistry.registerTask((ScheduledFuture) repeating); + } + + // ============================================ + // From "Registration Timing" section (lines 159-176) + // ============================================ + // Note: These examples show plugin lifecycle methods + // setup() - Configuration only + // start() - Register everything here +} diff --git a/app/src/main/java/org/example/docs/coreconcepts/ThreadingExample.java b/app/src/main/java/org/example/docs/coreconcepts/ThreadingExample.java new file mode 100644 index 0000000..3dcbe81 --- /dev/null +++ b/app/src/main/java/org/example/docs/coreconcepts/ThreadingExample.java @@ -0,0 +1,168 @@ +package org.example.docs.coreconcepts; + +// Example from: hytale-docs/content/core-concepts/threading.en.md +// This file tests that ALL threading documentation examples compile correctly + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Tests ALL threading.en.md documentation examples + */ +public class ThreadingExample { + + // ============================================ + // From "Per-World Threading" section (lines 17-25) + // ============================================ + public void perWorldThreadingExample(Player player) { + BlockType blockType = null; // Placeholder + Object position = null; // Placeholder + + World world = player.getWorld(); + + // Check if we're on this world's thread + if (world.isInThread()) { + // Safe to modify world state + // world.setBlock(position, blockType); + } + } + + // ============================================ + // From "Async Operations" section (lines 35-49) + // Note: Fixed to use PlayerRef.sendMessage() instead of getPlayer() + // ============================================ + public void asyncOperationsExample(Player player) { + PlayerRef ref = player.getPlayerRef(); + World world = player.getWorld(); + + CompletableFuture.supplyAsync(() -> { + // This runs on a worker thread + return loadDataFromDatabase(ref.getUuid()); + }).thenAccept(data -> { + // PlayerRef.sendMessage() is thread-safe + ref.sendMessage(Message.raw("Data loaded: " + data)); + }); + } + + private String loadDataFromDatabase(UUID uuid) { + return "sample data"; + } + + // ============================================ + // From "PlayerRef" section (lines 63-80) + // Note: Fixed - removed getWorld() and getPlayer() which don't exist + // ============================================ + public void playerRefExample(Player player) { + // Store reference (thread-safe) + PlayerRef playerRef = player.getPlayerRef(); + + // Safe to access on any thread: + UUID uuid = playerRef.getUuid(); + String username = playerRef.getUsername(); + String language = playerRef.getLanguage(); + + // Get current world UUID (may change if player switches worlds) + UUID worldUuid = playerRef.getWorldUuid(); + + // Send message directly (thread-safe) + playerRef.sendMessage(Message.raw("Hello!")); + + // For ECS operations, get the entity reference + Ref entityRef = playerRef.getReference(); // null if not in world + } + + // ============================================ + // From "Checking Thread Context" section (lines 84-94) + // ============================================ + public void checkThreadContextExample(Player player) { + World world = player.getWorld(); + + // Check if on world's thread + if (!world.isInThread()) { + throw new IllegalStateException("Must be called from world thread!"); + } + + // Debug: print current thread + System.out.println("Current thread: " + Thread.currentThread().getName()); + } + + // ============================================ + // From "Database Operations" section (lines 100-115) + // Note: Fixed to use PlayerRef.sendMessage() + // ============================================ + public void savePlayer(Player player) { + PlayerRef ref = player.getPlayerRef(); + Object data = collectPlayerData(player); + + CompletableFuture.runAsync(() -> { + // Runs on worker thread + saveToDatabase(ref.getUuid(), data); + }).thenRun(() -> { + // Notify player (PlayerRef.sendMessage is thread-safe) + ref.sendMessage(Message.raw("Saved!")); + }); + } + + private Object collectPlayerData(Player player) { return null; } + private void saveToDatabase(UUID uuid, Object data) {} + + // ============================================ + // From "Loading Data on Join" section (lines 120-138) + // Note: Fixed to use Ref instead of Player + // ============================================ + public void loadingDataOnJoinExample(EventRegistry eventRegistry) { + eventRegistry.register(PlayerConnectEvent.class, event -> { + PlayerRef ref = event.getPlayerRef(); + World world = event.getWorld(); + + CompletableFuture.supplyAsync(() -> { + // Load data on worker thread + return loadFromDatabase(ref.getUuid()); + }).thenAccept(data -> { + if (world != null && data != null) { + world.execute(() -> { + // Back on world thread - safe to access ECS + Ref entityRef = ref.getReference(); + if (entityRef != null) { + applyData(entityRef, data); + } + }); + } + }); + }); + } + + private Object loadFromDatabase(UUID uuid) { return null; } + private void applyData(Ref entityRef, Object data) {} + + // ============================================ + // From "Async Events" section (lines 143-156) + // Note: PlayerChatEvent has String key, so use registerAsyncGlobal + // ============================================ + public void asyncEventsExample(EventRegistry eventRegistry) { + eventRegistry.registerAsyncGlobal( + PlayerChatEvent.class, + future -> future.thenApply(event -> { + // This runs asynchronously + // Can perform slow operations here + String filtered = filterMessage(event.getContent()); + event.setContent(filtered); + return event; + }) + ); + } + + private String filterMessage(String content) { + return content; + } +} diff --git a/app/src/main/java/org/example/docs/effects/DynamicLightsExample.java b/app/src/main/java/org/example/docs/effects/DynamicLightsExample.java new file mode 100644 index 0000000..2660916 --- /dev/null +++ b/app/src/main/java/org/example/docs/effects/DynamicLightsExample.java @@ -0,0 +1,217 @@ +package org.example.docs.effects; + +// Example from: hytale-docs/content/effects/dynamic-lights.en.md +// Tests the Dynamic Lights documentation examples +// +// FINDINGS: +// - getTaskRegistry().schedule() does NOT exist - TaskRegistry only has registerTask() +// - ref.getPlayer() does NOT exist - PlayerRef doesn't have this method +// - player.getEntityRef() does NOT exist - use player.getReference() +// - Need to use standard Java concurrency for scheduling + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.protocol.ColorLight; +import com.hypixel.hytale.server.core.modules.entity.component.DynamicLight; +import com.hypixel.hytale.server.core.modules.entity.component.PersistentDynamicLight; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Tests Dynamic Lights documentation examples + */ +public class DynamicLightsExample extends JavaPlugin { + + private ScheduledExecutorService scheduler; + + public DynamicLightsExample(JavaPluginInit init) { + super(init); + } + + @Override + protected void start() { + scheduler = Executors.newSingleThreadScheduledExecutor(); + } + + @Override + protected void shutdown() { + if (scheduler != null) { + scheduler.shutdown(); + } + } + + // ============================================= + // ColorLight Structure (DOCUMENTED - CORRECT) + // ============================================= + public void colorLightStructure() { + ColorLight light = new ColorLight( + (byte) 15, // Radius (0-255) + (byte) 255, // Red (0-255) + (byte) 100, // Green (0-255) + (byte) 50 // Blue (0-255) + ); + } + + // ============================================= + // DynamicLight Component (DOCUMENTED - CORRECT) + // ============================================= + public void addDynamicLight(Ref entityRef, ComponentAccessor componentAccessor) { + ColorLight light = new ColorLight( + (byte) 15, + (byte) 255, + (byte) 100, + (byte) 50 + ); + + DynamicLight dynamicLight = new DynamicLight(light); + componentAccessor.putComponent( + entityRef, + DynamicLight.getComponentType(), + dynamicLight + ); + } + + // ============================================= + // PersistentDynamicLight Component (DOCUMENTED - CORRECT) + // ============================================= + public void addPersistentLight(Ref entityRef, ComponentAccessor componentAccessor, ColorLight light) { + PersistentDynamicLight persistentLight = new PersistentDynamicLight(light); + componentAccessor.putComponent( + entityRef, + PersistentDynamicLight.getComponentType(), + persistentLight + ); + } + + // ============================================= + // Updating Light Properties (CORRECT - uses Store) + // ============================================= + public void updateLight(Ref entityRef, Store store) { + DynamicLight dynamicLight = store.getComponent( + entityRef, + DynamicLight.getComponentType() + ); + + if (dynamicLight != null) { + ColorLight newLight = new ColorLight( + (byte) 20, + (byte) 0, + (byte) 255, + (byte) 100 + ); + dynamicLight.setColorLight(newLight); + } + } + + // ============================================= + // Removing Light (DOCUMENTED - CORRECT) + // ============================================= + public void removeLight(Ref entityRef, ComponentAccessor componentAccessor) { + componentAccessor.removeComponent( + entityRef, + DynamicLight.getComponentType() + ); + } + + // ============================================= + // Common Light Configurations (DOCUMENTED - CORRECT) + // ============================================= + public void lightConfigurations() { + ColorLight fireLight = new ColorLight((byte) 12, (byte) 255, (byte) 150, (byte) 50); + ColorLight iceLight = new ColorLight((byte) 10, (byte) 100, (byte) 200, (byte) 255); + ColorLight magicLight = new ColorLight((byte) 15, (byte) 200, (byte) 50, (byte) 255); + ColorLight healLight = new ColorLight((byte) 10, (byte) 50, (byte) 255, (byte) 100); + } + + // ============================================= + // Adding a Glow Effect to a Player - CORRECTED VERSION + // DOCUMENTED CODE HAS ISSUES: + // - ref.getPlayer() does NOT exist + // - getTaskRegistry().schedule() does NOT exist + // - player.getEntityRef() should be player.getReference() + // ============================================= + public void addGlowEffectCorrected(Player player, ComponentAccessor componentAccessor) { + PlayerRef ref = player.getPlayerRef(); + World world = player.getWorld(); + + ColorLight glow = new ColorLight( + (byte) 8, + (byte) 255, + (byte) 215, + (byte) 0 + ); + + DynamicLight light = new DynamicLight(glow); + // CORRECT: Use player.getReference() instead of player.getEntityRef() + Ref entityRef = player.getReference(); + componentAccessor.putComponent( + entityRef, + DynamicLight.getComponentType(), + light + ); + + // CORRECT: Use standard Java scheduling instead of getTaskRegistry().schedule() + scheduler.schedule(() -> { + world.execute(() -> { + // Check if entity is still valid + if (entityRef.isValid()) { + componentAccessor.removeComponent( + entityRef, + DynamicLight.getComponentType() + ); + } + }); + }, 3, TimeUnit.SECONDS); + } + + // ============================================= + // Pulsing Light Effect (CORRECTED - uses Store) + // ============================================= + private final Map, Integer> pulsingEntities = new ConcurrentHashMap<>(); + + public void onTick(float deltaTime, Store store) { + for (Map.Entry, Integer> entry : pulsingEntities.entrySet()) { + Ref entityRef = entry.getKey(); + int tick = entry.getValue() + 1; + entry.setValue(tick); + + double pulse = Math.sin(tick * 0.1) * 0.5 + 0.5; + byte radius = (byte) (5 + pulse * 10); + + ColorLight light = new ColorLight( + radius, + (byte) 255, + (byte) (int)(100 + pulse * 100), + (byte) 50 + ); + + DynamicLight dynamicLight = store.getComponent( + entityRef, + DynamicLight.getComponentType() + ); + + if (dynamicLight != null) { + dynamicLight.setColorLight(light); + } + } + } + + public void startPulsingLight(Ref entityRef) { + pulsingEntities.put(entityRef, 0); + } + + public void stopPulsingLight(Ref entityRef) { + pulsingEntities.remove(entityRef); + } +} diff --git a/app/src/main/java/org/example/docs/effects/EffectsIndexExample.java b/app/src/main/java/org/example/docs/effects/EffectsIndexExample.java new file mode 100644 index 0000000..62ccb6c --- /dev/null +++ b/app/src/main/java/org/example/docs/effects/EffectsIndexExample.java @@ -0,0 +1,81 @@ +package org.example.docs.effects; + +// Example from: hytale-docs/content/effects/_index.en.md +// Tests the Effects index page documentation examples +// +// FINDINGS: +// - Vector3d is from com.hypixel.hytale.math.vector.Vector3d, NOT org.joml +// - store.getComponent needs a Store, not EntityStore directly +// - player.getEntityRef() does NOT exist - use player.getReference() + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.universe.world.ParticleUtil; +import com.hypixel.hytale.protocol.ColorLight; +import com.hypixel.hytale.server.core.modules.entity.component.DynamicLight; +import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent; +import com.hypixel.hytale.server.core.asset.type.entityeffect.config.EntityEffect; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.math.vector.Vector3d; + +/** + * Tests Effects index documentation examples + */ +public class EffectsIndexExample extends JavaPlugin { + + public EffectsIndexExample(JavaPluginInit init) { + super(init); + } + + // ============================================= + // Quick Start: Spawning Particles + // ============================================= + public void spawnParticleExample(Vector3d position, ComponentAccessor componentAccessor) { + // Spawn particles at a position + ParticleUtil.spawnParticleEffect( + "explosion_small", // Particle system ID + position, // Vector3d position + componentAccessor + ); + } + + // ============================================= + // Quick Start: Adding Dynamic Light + // ============================================= + public void dynamicLightExample(Ref entityRef, ComponentAccessor componentAccessor) { + // Create a colored light (radius, R, G, B) + ColorLight light = new ColorLight( + (byte) 15, // Radius + (byte) 255, // Red + (byte) 100, // Green + (byte) 50 // Blue + ); + + // Add to entity + DynamicLight dynamicLight = new DynamicLight(light); + componentAccessor.putComponent(entityRef, DynamicLight.getComponentType(), dynamicLight); + } + + // ============================================= + // Quick Start: Applying Entity Effects + // NOTE: Documentation uses store.getComponent() but EntityStore != Store + // The actual API requires Store which is obtained from EntityStore.getStore() + // ============================================= + public void entityEffectExample(Ref entityRef, Store store, ComponentAccessor componentAccessor) { + // Get effect controller + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + // Apply effect + EntityEffect effect = EntityEffect.getAssetMap().getAsset("fire_resistance"); + if (controller != null && effect != null) { + controller.addEffect(entityRef, effect, componentAccessor); + } + } +} diff --git a/app/src/main/java/org/example/docs/effects/EntityEffectsExample.java b/app/src/main/java/org/example/docs/effects/EntityEffectsExample.java new file mode 100644 index 0000000..65e9617 --- /dev/null +++ b/app/src/main/java/org/example/docs/effects/EntityEffectsExample.java @@ -0,0 +1,318 @@ +package org.example.docs.effects; + +// Example from: hytale-docs/content/effects/entity-effects.en.md +// Tests the Entity Effects documentation examples +// +// FINDINGS (DOCUMENTED CODE HAS ISSUES): +// - OverlapBehavior is at com.hypixel.hytale.server.core.asset.type.entityeffect.config.OverlapBehavior +// - BuffCommand.execute() returns void, should return CompletableFuture +// - ctx.getArg("name", Type.class) does NOT exist - use requiredArg.get(ctx) +// - target.getPlayer() does NOT exist - PlayerRef doesn't have this method +// - ctx.getSender() should be ctx.sender() +// - target.getName() should be target.getUsername() +// - player.sendMessage("text") should use Message.raw("text") +// - player.getEntityRef() does NOT exist - use player.getReference() +// - store.getComponent() requires Store, not EntityStore + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent; +import com.hypixel.hytale.server.core.asset.type.entityeffect.config.OverlapBehavior; +import com.hypixel.hytale.server.core.asset.type.entityeffect.config.RemovalBehavior; +import com.hypixel.hytale.server.core.asset.type.entityeffect.config.EntityEffect; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.ParticleUtil; +import com.hypixel.hytale.server.core.modules.entity.component.DynamicLight; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.protocol.ColorLight; +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg; + +import java.util.concurrent.CompletableFuture; + +/** + * Tests Entity Effects documentation examples + */ +public class EntityEffectsExample extends JavaPlugin { + + public EntityEffectsExample(JavaPluginInit init) { + super(init); + } + + // ============================================= + // Accessing Effect Assets (DOCUMENTED - CORRECT) + // ============================================= + public void accessEffectAssets() { + // Get effect by ID + EntityEffect effect = EntityEffect.getAssetMap().getAsset("fire_resistance"); + + // Get effect index for operations + if (effect != null) { + int effectIndex = EntityEffect.getAssetMap().getIndex(effect.getId()); + } + } + + // ============================================= + // EffectControllerComponent - Getting controller + // CORRECTED: Uses Store instead of EntityStore + // ============================================= + public void getEffectController(Ref entityRef, Store store) { + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + } + + // ============================================= + // Adding Effects - Basic Addition (CORRECTED) + // ============================================= + public void addBasicEffect(Ref entityRef, Store store, ComponentAccessor componentAccessor) { + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + EntityEffect effect = EntityEffect.getAssetMap().getAsset("speed_boost"); + + if (controller != null && effect != null) { + controller.addEffect( + entityRef, + effect, + componentAccessor + ); + } + } + + // ============================================= + // Adding Effects - With Custom Duration (CORRECTED) + // ============================================= + public void addEffectWithDuration(Ref entityRef, Store store, ComponentAccessor componentAccessor) { + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + EntityEffect effect = EntityEffect.getAssetMap().getAsset("speed_boost"); + + if (controller != null && effect != null) { + controller.addEffect( + entityRef, + effect, + 100.0f, // Duration in seconds + OverlapBehavior.EXTEND, // How to handle overlap + componentAccessor + ); + } + } + + // ============================================= + // Adding Effects - Infinite Effects (CORRECTED) + // ============================================= + public void addInfiniteEffect(Ref entityRef, Store store, ComponentAccessor componentAccessor) { + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + EntityEffect effect = EntityEffect.getAssetMap().getAsset("permanent_buff"); + + if (controller != null && effect != null) { + controller.addInfiniteEffect( + entityRef, + EntityEffect.getAssetMap().getIndex(effect.getId()), + effect, + componentAccessor + ); + } + } + + // ============================================= + // Removing Effects (CORRECTED) + // ============================================= + public void removeEffect(Ref entityRef, Store store, ComponentAccessor componentAccessor) { + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + EntityEffect effect = EntityEffect.getAssetMap().getAsset("speed_boost"); + + if (controller != null && effect != null) { + controller.removeEffect( + entityRef, + EntityEffect.getAssetMap().getIndex(effect.getId()), + componentAccessor + ); + } + } + + // ============================================= + // Overlap Behaviors Usage (CORRECTED) + // ============================================= + public void overlapBehaviorExamples(Ref entityRef, Store store, ComponentAccessor componentAccessor) { + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + EntityEffect effect = EntityEffect.getAssetMap().getAsset("buff"); + + if (controller != null && effect != null) { + // Extend duration if already applied + controller.addEffect(entityRef, effect, 30.0f, OverlapBehavior.EXTEND, componentAccessor); + + // Replace with fresh duration + controller.addEffect(entityRef, effect, 30.0f, OverlapBehavior.OVERWRITE, componentAccessor); + + // Only apply if not already active + controller.addEffect(entityRef, effect, 30.0f, OverlapBehavior.IGNORE, componentAccessor); + } + } + + // ============================================= + // Buff Command - CORRECTED VERSION + // Fixed: return type, argument API, sender API, PlayerRef API + // ============================================= + public static class BuffCommandCorrected extends AbstractCommand { + private final RequiredArg playerArg; + private final RequiredArg effectArg; + private final RequiredArg durationArg; + + // Would be injected during plugin init + private Store store; + private ComponentAccessor componentAccessor; + + public BuffCommandCorrected() { + super("buff", "effects.command.buff.description"); + this.playerArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF); + this.effectArg = withRequiredArg("effect", "Effect ID", ArgTypes.STRING); + this.durationArg = withRequiredArg("duration", "Duration in seconds", ArgTypes.INTEGER); + } + + @Override + protected CompletableFuture execute(CommandContext ctx) { + PlayerRef target = playerArg.get(ctx); + String effectId = effectArg.get(ctx); + int duration = durationArg.get(ctx); + + // CORRECTED: Use target.getReference() - PlayerRef doesn't have getPlayer() + Ref entityRef = target.getReference(); + if (entityRef == null || !entityRef.isValid()) { + ctx.sender().sendMessage(Message.raw("Player not found")); + return null; + } + + EntityEffect effect = EntityEffect.getAssetMap().getAsset(effectId); + if (effect == null) { + ctx.sender().sendMessage(Message.raw("Unknown effect: " + effectId)); + return null; + } + + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + if (controller != null) { + controller.addEffect( + entityRef, + effect, + (float) duration, + OverlapBehavior.EXTEND, + componentAccessor + ); + } + + // CORRECTED: target.getUsername() instead of target.getName() + ctx.sender().sendMessage(Message.raw("Applied " + effectId + " to " + target.getUsername())); + return null; + } + } + + // ============================================= + // Damage Over Time Effect - CORRECTED + // Uses player.getReference() and Message.raw() + // ============================================= + public void applyPoison(Player player, float duration, Store store, ComponentAccessor componentAccessor) { + EntityEffect poison = EntityEffect.getAssetMap().getAsset("poison"); + + // CORRECTED: Use player.getReference() instead of player.getEntityRef() + Ref entityRef = player.getReference(); + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + if (controller != null && poison != null) { + controller.addEffect( + entityRef, + poison, + duration, + OverlapBehavior.EXTEND, + componentAccessor + ); + } + + // CORRECTED: Use Message.raw() + player.getPlayerRef().sendMessage(Message.raw("You have been poisoned!")); + } + + // ============================================= + // Clear All Effects - CORRECTED + // ============================================= + public void clearAllEffects(Player player, Store store, ComponentAccessor componentAccessor) { + Ref entityRef = player.getReference(); + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + if (controller != null) { + controller.clearEffects( + entityRef, + componentAccessor + ); + } + + // CORRECTED: Use Message.raw() + player.getPlayerRef().sendMessage(Message.raw("All effects cleared!")); + } + + // ============================================= + // Combining with Other Effects - CORRECTED + // player.getPosition() does NOT exist on Player + // ============================================= + public void applyMagicBuff(Player player, Store store, ComponentAccessor componentAccessor) { + EntityEffect buff = EntityEffect.getAssetMap().getAsset("magic_power"); + Ref entityRef = player.getReference(); + + EffectControllerComponent controller = store.getComponent( + entityRef, + EffectControllerComponent.getComponentType() + ); + + if (controller != null && buff != null) { + controller.addEffect(entityRef, buff, componentAccessor); + } + + // Add visual glow + ColorLight glow = new ColorLight((byte) 10, (byte) 200, (byte) 50, (byte) 255); + DynamicLight light = new DynamicLight(glow); + componentAccessor.putComponent( + entityRef, + DynamicLight.getComponentType(), + light + ); + + // NOTE: player.getPosition() does NOT exist + // Would need to get position from TransformComponent via the entity store + } +} diff --git a/app/src/main/java/org/example/docs/effects/ParticlesExample.java b/app/src/main/java/org/example/docs/effects/ParticlesExample.java new file mode 100644 index 0000000..7432f88 --- /dev/null +++ b/app/src/main/java/org/example/docs/effects/ParticlesExample.java @@ -0,0 +1,144 @@ +package org.example.docs.effects; + +// Example from: hytale-docs/content/effects/particles.en.md +// Tests the Particles documentation examples +// +// FINDINGS (DOCUMENTED CODE HAS ISSUES): +// - Vector3d is from com.hypixel.hytale.math.vector.Vector3d, NOT org.joml +// - event.getPosition().x() - Vector3i uses getX(), not x() (but also has public fields x,y,z) +// - target.getPosition() - Entity doesn't have getPosition() directly +// - player.getPosition() - Player doesn't have getPosition() +// - player.getUuid() - should be player.getPlayerRef().getUuid() + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.universe.world.ParticleUtil; +import com.hypixel.hytale.server.core.asset.type.particle.config.ParticleSystem; +import com.hypixel.hytale.server.core.asset.type.particle.config.ParticleSpawner; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.Entity; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent; +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3i; + +import java.awt.Color; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.UUID; + +/** + * Tests Particles documentation examples + */ +public class ParticlesExample extends JavaPlugin { + + private ComponentAccessor componentAccessor; + + public ParticlesExample(JavaPluginInit init) { + super(init); + } + + // ============================================= + // Basic Spawn (DOCUMENTED - CORRECT) + // ============================================= + public void basicSpawn(Vector3d position) { + ParticleUtil.spawnParticleEffect( + "explosion_small", + position, + componentAccessor + ); + } + + // ============================================= + // Spawn for specific players (DOCUMENTED - CORRECT) + // ============================================= + public void spawnForPlayers(Vector3d position, List> playerRefs) { + ParticleUtil.spawnParticleEffect( + "magic_trail", + position, + playerRefs, + componentAccessor + ); + } + + // ============================================= + // Advanced: With source entity reference (DOCUMENTED - CORRECT) + // ============================================= + public void advancedSpawn(Vector3d position, Ref sourceEntityRef, List> playerRefs) { + ParticleUtil.spawnParticleEffect( + "attack_swing", + position, + sourceEntityRef, + playerRefs, + componentAccessor + ); + } + + // ============================================= + // Accessing Particle Assets (DOCUMENTED - CORRECT) + // ============================================= + public void accessAssets() { + ParticleSystem system = ParticleSystem.getAssetMap().getAsset("explosion_large"); + ParticleSpawner spawner = ParticleSpawner.getAssetMap().getAsset("fire_spawner"); + } + + // ============================================= + // Event-Based Particles: Block Break + // DOCUMENTED CODE HAS ISSUE: + // event.getPosition().x() should work since Vector3i has public field x + // But documentation uses x() method which doesn't exist + // CORRECT: Use .x, .y, .z public fields OR getX(), getY(), getZ() + // ============================================= + public void onBlockBreak(BreakBlockEvent event) { + // CORRECTED: Use getTargetBlock(), not getPosition() + Vector3i blockPos = event.getTargetBlock(); + + // Use public fields x, y, z + Vector3d pos = new Vector3d( + blockPos.x + 0.5, + blockPos.y + 0.5, + blockPos.z + 0.5 + ); + + ParticleUtil.spawnParticleEffect( + "block_break_particles", + pos, + componentAccessor + ); + } + + // ============================================= + // Aura Manager - CORRECTED + // player.getUuid() should be player.getPlayerRef().getUuid() + // player.getPosition() doesn't exist + // ============================================= + private final Map activeAuras = new HashMap<>(); + + public void enableAura(Player player) { + // CORRECTED: Use player.getPlayerRef().getUuid() + activeAuras.put(player.getPlayerRef().getUuid(), true); + } + + // NOTE: Tick-based aura spawning would need to get player position + // from TransformComponent, not from player.getPosition() + + // ============================================= + // Efficient spawning pattern (DOCUMENTED - CORRECT pattern) + // ============================================= + public void spawnEfficiently(Vector3d position, List> nearbyPlayerRefs) { + if (!nearbyPlayerRefs.isEmpty()) { + ParticleUtil.spawnParticleEffect( + "my_effect", + position, + nearbyPlayerRefs, + componentAccessor + ); + } + } +} diff --git a/app/src/main/java/org/example/docs/entities/EntitiesIndexExample.java b/app/src/main/java/org/example/docs/entities/EntitiesIndexExample.java new file mode 100644 index 0000000..43a3b6a --- /dev/null +++ b/app/src/main/java/org/example/docs/entities/EntitiesIndexExample.java @@ -0,0 +1,49 @@ +package org.example.docs.entities; + +// Example from: hytale-docs/content/entities/_index.en.md +// Tests the "Quick Example" code block + +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; + +/** + * Tests _index.en.md documentation + * + * NOTE: The doc shows player.getPosition() but this method doesn't exist. + * Position is accessed via getTransformComponent().getPosition() (deprecated) + * or through ECS component access. + */ +public class EntitiesIndexExample extends JavaPlugin { + + public EntitiesIndexExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // From "Quick Example" section - CORRECTED version + getEventRegistry().register(PlayerConnectEvent.class, event -> { + // getPlayer() is @Deprecated but exists + Player player = event.getPlayer(); + + if (player != null) { + // getPosition() doesn't exist on Player - use TransformComponent + // getTransformComponent() is deprecated but works + TransformComponent transform = player.getTransformComponent(); + Vector3d pos = transform.getPosition(); + + // Get world - this works + World world = player.getWorld(); + + // sendMessage requires Message object, not String + player.sendMessage(Message.raw("Welcome at " + pos.toString())); + } + }); + } +} diff --git a/app/src/main/java/org/example/docs/entities/EntityComponentsExample.java b/app/src/main/java/org/example/docs/entities/EntityComponentsExample.java new file mode 100644 index 0000000..64c5cb5 --- /dev/null +++ b/app/src/main/java/org/example/docs/entities/EntityComponentsExample.java @@ -0,0 +1,175 @@ +package org.example.docs.entities; + +// Example from: hytale-docs/content/entities/entity-components.en.md +// Tests the entity component documentation examples +// +// CRITICAL FINDING: +// The documentation shows: entityRef.getComponent(type) and entityRef.hasComponent(type) +// But Ref does NOT have these methods! +// Correct pattern: store.getComponent(ref, type) where store = ref.getStore() + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.component.DisplayNameComponent; +import com.hypixel.hytale.server.core.modules.entity.component.BoundingBox; +import com.hypixel.hytale.server.core.modules.entity.component.EntityScaleComponent; +import com.hypixel.hytale.server.core.modules.entity.component.DynamicLight; +import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation; +import com.hypixel.hytale.server.core.modules.entity.component.Invulnerable; +import com.hypixel.hytale.server.core.modules.entity.component.Intangible; +import com.hypixel.hytale.server.core.modules.entity.component.Interactable; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Archetype; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.math.shape.Box; +import com.hypixel.hytale.protocol.ColorLight; +import com.hypixel.hytale.server.core.Message; + +import java.util.logging.Level; + +/** + * Tests entity-components.en.md documentation + * + * IMPORTANT: The documentation shows entityRef.getComponent(type) but this is WRONG! + * The correct pattern is: + * Store store = ref.getStore(); + * Component comp = store.getComponent(ref, ComponentType); + * + * Ref only has: + * - getStore() - returns Store + * - getIndex() - returns int + * - isValid() - returns boolean + * - validate() - throws if invalid + */ +public class EntityComponentsExample extends JavaPlugin { + + public EntityComponentsExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + + // Get ECS reference + Ref ref = playerRef.getReference(); + if (ref == null || !ref.isValid()) { + return; + } + + // CORRECT PATTERN: get store, then get component from store + Store store = ref.getStore(); + + // ============================================= + // TransformComponent - CORRECT PATTERN + // ============================================= + ComponentType transformType = + TransformComponent.getComponentType(); + TransformComponent transform = store.getComponent(ref, transformType); + + if (transform != null) { + // Get current position + Vector3d pos = transform.getPosition(); + getLogger().at(Level.INFO).log("Entity at: " + pos.x + ", " + pos.y + ", " + pos.z); + + // Get rotation + Vector3f rotation = transform.getRotation(); + } + + // ============================================= + // BoundingBox - CORRECT PATTERN + // ============================================= + ComponentType boundsType = + BoundingBox.getComponentType(); + BoundingBox bounds = store.getComponent(ref, boundsType); + + if (bounds != null) { + Box box = bounds.getBoundingBox(); + // Box has methods like width(), height(), depth() + } + + // ============================================= + // DisplayNameComponent - CORRECT PATTERN + // ============================================= + ComponentType nameType = + DisplayNameComponent.getComponentType(); + DisplayNameComponent nameComp = store.getComponent(ref, nameType); + + if (nameComp != null) { + Message name = nameComp.getDisplayName(); + } + + // ============================================= + // EntityScaleComponent - CORRECT PATTERN + // ============================================= + ComponentType scaleType = + EntityScaleComponent.getComponentType(); + EntityScaleComponent scale = store.getComponent(ref, scaleType); + + if (scale != null) { + float currentScale = scale.getScale(); + // scale.setScale(2.0f); // Would make entity 2x larger + } + + // ============================================= + // DynamicLight - CORRECT PATTERN + // ============================================= + ComponentType lightType = + DynamicLight.getComponentType(); + DynamicLight light = store.getComponent(ref, lightType); + + if (light != null) { + ColorLight colorLight = light.getColorLight(); + // colorLight has public fields: radius, red, green, blue + } + + // ============================================= + // HeadRotation - CORRECT PATTERN + // ============================================= + ComponentType headType = + HeadRotation.getComponentType(); + HeadRotation head = store.getComponent(ref, headType); + + if (head != null) { + Vector3f rot = head.getRotation(); + Vector3d direction = head.getDirection(); + } + + // ============================================= + // Singleton Marker Components + // ============================================= + // To check if an entity has a marker component, use archetype + Archetype archetype = store.getArchetype(ref); + + // Check for Invulnerable + ComponentType invulnType = + Invulnerable.getComponentType(); + boolean isInvulnerable = archetype.contains(invulnType); + + // Check for Intangible + ComponentType intangibleType = + Intangible.getComponentType(); + boolean isIntangible = archetype.contains(intangibleType); + + // Check for Interactable + ComponentType interactableType = + Interactable.getComponentType(); + boolean isInteractable = archetype.contains(interactableType); + + // ============================================= + // WRONG PATTERNS (from the documentation): + // ============================================= + // entityRef.getComponent(transformType); // WRONG - Ref doesn't have getComponent + // entityRef.hasComponent(type); // WRONG - Ref doesn't have hasComponent + // entityRef.getAccessor(); // WRONG - Ref doesn't have getAccessor + }); + } +} diff --git a/app/src/main/java/org/example/docs/entities/EntityHierarchyExample.java b/app/src/main/java/org/example/docs/entities/EntityHierarchyExample.java new file mode 100644 index 0000000..4e8f937 --- /dev/null +++ b/app/src/main/java/org/example/docs/entities/EntityHierarchyExample.java @@ -0,0 +1,199 @@ +package org.example.docs.entities; + +// Example from: hytale-docs/content/entities/entity-hierarchy.en.md +// This file tests the entity hierarchy documentation examples +// +// CRITICAL FINDING: +// The documentation describes a traditional OOP entity API that does NOT exist. +// Hytale uses ECS (Entity Component System) architecture where: +// - Entity is a Component, not a standalone class with methods +// - Position is accessed via TransformComponent +// - Health, damage, effects are managed through separate systems/components +// - Methods like getPosition(), setPosition(), getHealth(), damage() DON'T EXIST + +import com.hypixel.hytale.server.core.entity.Entity; +import com.hypixel.hytale.server.core.entity.LivingEntity; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.math.vector.Vector3d; +import java.util.UUID; +import java.util.logging.Level; + +/** + * Tests entity-hierarchy.en.md documentation + * + * IMPORTANT: Most methods described in the documentation do NOT exist. + * This file demonstrates what ACTUALLY works vs what the doc claims. + */ +public class EntityHierarchyExample extends JavaPlugin { + + public EntityHierarchyExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // Test Player methods that actually exist + getEventRegistry().register(PlayerConnectEvent.class, event -> { + Player player = event.getPlayer(); + PlayerRef playerRef = event.getPlayerRef(); + World world = event.getWorld(); + + if (player != null) { + // ============================================= + // METHODS THAT ACTUALLY EXIST on Player: + // ============================================= + + // Send message (requires Message object, not String!) + player.sendMessage(Message.raw("Hello!")); + + // Check permissions - EXISTS + boolean hasAdmin = player.hasPermission("admin.teleport"); + + // Get display name - EXISTS + String displayName = player.getDisplayName(); + + // Get world - EXISTS on Entity + World playerWorld = player.getWorld(); + + // Get inventory - EXISTS on LivingEntity + Inventory inventory = player.getInventory(); + + // Get UUID - EXISTS (but deprecated) + UUID uuid = player.getUuid(); + + // wasRemoved() - EXISTS (not isRemoved()) + boolean removed = player.wasRemoved(); + + // getTransformComponent() - EXISTS but deprecated + TransformComponent transform = player.getTransformComponent(); + Vector3d position = transform.getPosition(); + + // ============================================= + // METHODS THAT DON'T EXIST (documented but wrong): + // ============================================= + // player.getPosition() - DOESN'T EXIST + // player.setPosition() - DOESN'T EXIST (use moveTo with ECS) + // player.isValid() - DOESN'T EXIST + // player.getId() - use getNetworkId() (deprecated) + // player.getEntityType() - DOESN'T EXIST + // player.hasComponent() - DOESN'T EXIST (Entity IS a Component) + // player.getComponent() - DOESN'T EXIST (use Store.getComponent) + // player.getName() - use getDisplayName() or playerRef.getUsername() + // player.sendMessage(String) - requires Message.raw() + // player.sendTitle() - DOESN'T EXIST + // player.sendActionBar() - DOESN'T EXIST + // player.getHealth() - DOESN'T EXIST + // player.setHealth() - DOESN'T EXIST + // player.damage() - DOESN'T EXIST + // player.addEffect() - DOESN'T EXIST + // player.getVelocity() - DOESN'T EXIST (use Velocity component) + // player.setVelocity() - DOESN'T EXIST + // player.giveItem() - DOESN'T EXIST (use inventory.addItem) + // player.kick() - DOESN'T EXIST + } + + // ============================================= + // PlayerRef - what actually exists: + // ============================================= + String username = playerRef.getUsername(); + UUID uuid = playerRef.getUuid(); + UUID worldUuid = playerRef.getWorldUuid(); + + // Send message via PlayerRef - EXISTS + playerRef.sendMessage(Message.raw("Hello via PlayerRef!")); + + // Get ECS reference - EXISTS + Ref ref = playerRef.getReference(); + if (ref != null && ref.isValid()) { + // This is how you access components in ECS + Store store = ref.getStore(); + Player playerComponent = store.getComponent(ref, Player.getComponentType()); + TransformComponent transformComponent = store.getComponent(ref, TransformComponent.getComponentType()); + } + }); + } + + // ============================================= + // LivingEntity - what actually exists: + // ============================================= + public void livingEntityExample(LivingEntity living) { + // getInventory() - EXISTS + Inventory inventory = living.getInventory(); + + // getWorld() - EXISTS (inherited from Entity) + World world = living.getWorld(); + + // wasRemoved() - EXISTS + boolean removed = living.wasRemoved(); + + // getCurrentFallDistance() - EXISTS + double fallDistance = living.getCurrentFallDistance(); + + // getStatModifiersManager() - EXISTS + var statManager = living.getStatModifiersManager(); + + // ============================================= + // Methods that DON'T EXIST on LivingEntity: + // ============================================= + // getHealth(), setHealth(), getMaxHealth(), setMaxHealth() + // isDead(), heal() + // damage(double, DamageSource) + // addEffect(), removeEffect(), hasEffect(), clearEffects() + // getVelocity(), setVelocity(), knockback() + // isOnGround(), isSprinting(), isSneaking() + // setInvulnerable(), isInvulnerable() + // getKiller(), getLastDamageSource() + } + + // ============================================= + // Entity - what actually exists: + // ============================================= + public void entityExample(Entity entity) { + // getWorld() - EXISTS + World world = entity.getWorld(); + + // getUuid() - EXISTS but deprecated + UUID uuid = entity.getUuid(); + + // wasRemoved() - EXISTS (not isRemoved()) + boolean removed = entity.wasRemoved(); + + // remove() - EXISTS + // entity.remove(); + + // getNetworkId() - EXISTS but deprecated (not getId()) + int networkId = entity.getNetworkId(); + + // getTransformComponent() - EXISTS but deprecated + TransformComponent transform = entity.getTransformComponent(); + + // getReference() - EXISTS + Ref ref = entity.getReference(); + + // ============================================= + // Methods that DON'T EXIST on Entity: + // ============================================= + // getPosition(), setPosition() + // getRotation(), setRotation() + // getId() - use getNetworkId() + // isValid() - check ref.isValid() + // getEntityType() + // getChunk() + // getBoundingBox() + // hasComponent(), getComponent(), addComponent(), removeComponent() + // isRemoved() - use wasRemoved() + // getTicksLived() + } +} diff --git a/app/src/main/java/org/example/docs/entities/PlayerApiExample.java b/app/src/main/java/org/example/docs/entities/PlayerApiExample.java new file mode 100644 index 0000000..840c07b --- /dev/null +++ b/app/src/main/java/org/example/docs/entities/PlayerApiExample.java @@ -0,0 +1,164 @@ +package org.example.docs.entities; + +// Example from: hytale-docs/content/entities/player-api.en.md +// Tests the Player API documentation examples +// +// FINDINGS: +// Many methods documented do NOT exist on Player class. +// Player is a LivingEntity with limited direct methods. + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.protocol.GameMode; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.math.vector.Transform; + +import java.util.UUID; +import java.util.logging.Level; + +/** + * Tests player-api.en.md documentation + * + * IMPORTANT: Many methods in the documentation DON'T EXIST on Player: + * - getPosition(), setPosition() - use TransformComponent + * - getVelocity(), setVelocity(), addVelocity(), knockback() - don't exist + * - isSprinting(), setSprinting(), isSneaking(), setSneaking() - don't exist + * - isFlying(), setFlying(), isSwimming() - don't exist + * - isOnGround() - doesn't exist + * - isOp(), setOp(), addPermission(), removePermission(), getPermissions() - don't exist + * - giveItem(), dropItem(), getHeldItem() - don't exist + * - openInventory(), closeInventory(), getCursorItem() - don't exist + * - kick() - use playerRef.getPacketHandler().disconnect() + */ +public class PlayerApiExample extends JavaPlugin { + + public PlayerApiExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + Player player = event.getPlayer(); + + // ============================================= + // Methods that ACTUALLY EXIST on Player: + // ============================================= + + if (player != null) { + // getUuid() - EXISTS (deprecated) + UUID uuid = player.getUuid(); + + // getDisplayName() - EXISTS + String displayName = player.getDisplayName(); + + // sendMessage(Message) - EXISTS + player.sendMessage(Message.raw("Hello!")); + + // hasPermission(String) - EXISTS + boolean hasAdmin = player.hasPermission("admin.perm"); + + // getGameMode() - EXISTS + GameMode mode = player.getGameMode(); + + // getInventory() - EXISTS (inherited from LivingEntity) + Inventory inventory = player.getInventory(); + + // getViewRadius() / getClientViewRadius() - EXISTS + int viewRadius = player.getViewRadius(); + int clientViewRadius = player.getClientViewRadius(); + + // getWorld() - EXISTS (inherited from Entity) + var world = player.getWorld(); + } + + // ============================================= + // PlayerRef - Methods that EXIST: + // ============================================= + + // getUuid() - EXISTS + UUID uuid = playerRef.getUuid(); + + // getUsername() - EXISTS + String username = playerRef.getUsername(); + + // getWorldUuid() - EXISTS + UUID worldUuid = playerRef.getWorldUuid(); + + // sendMessage(Message) - EXISTS + playerRef.sendMessage(Message.raw("Hello via PlayerRef!")); + + // getReference() - EXISTS + Ref ref = playerRef.getReference(); + + // getPacketHandler() - EXISTS + var packetHandler = playerRef.getPacketHandler(); + + // getTransform() - EXISTS + Transform transform = playerRef.getTransform(); + if (transform != null) { + Vector3d position = transform.getPosition(); + Vector3f rotation = transform.getRotation(); + } + + // getHeadRotation() - EXISTS + Vector3f headRotation = playerRef.getHeadRotation(); + + // isValid() - EXISTS + boolean isValid = playerRef.isValid(); + + // referToServer() - EXISTS + // playerRef.referToServer("play.example.com", 25565); + + // getChunkTracker() - EXISTS + var chunkTracker = playerRef.getChunkTracker(); + + // ============================================= + // WRONG PATTERNS (from documentation): + // ============================================= + // player.getPosition() - DOESN'T EXIST + // player.setPosition(Vector3d) - DOESN'T EXIST + // player.getVelocity() - DOESN'T EXIST + // player.setVelocity(Vector3d) - DOESN'T EXIST + // player.addVelocity(Vector3d) - DOESN'T EXIST + // player.knockback(Vector3d) - DOESN'T EXIST + // player.isSprinting() - DOESN'T EXIST + // player.setSprinting(boolean) - DOESN'T EXIST + // player.isSneaking() - DOESN'T EXIST + // player.setSneaking(boolean) - DOESN'T EXIST + // player.isFlying() - DOESN'T EXIST + // player.setFlying(boolean) - DOESN'T EXIST + // player.isSwimming() - DOESN'T EXIST + // player.isOnGround() - DOESN'T EXIST + // player.getEyePosition() - DOESN'T EXIST + // player.getEyeDirection() - DOESN'T EXIST + // player.isOp() - DOESN'T EXIST + // player.setOp(boolean) - DOESN'T EXIST + // player.addPermission(String) - DOESN'T EXIST + // player.removePermission(String) - DOESN'T EXIST + // player.getPermissions() - DOESN'T EXIST + // player.giveItem(ItemStack) - DOESN'T EXIST + // player.dropItem(ItemStack) - DOESN'T EXIST + // player.getHeldItem() - DOESN'T EXIST + // player.setHeldItem(ItemStack) - DOESN'T EXIST + // player.openInventory(Inventory) - DOESN'T EXIST + // player.closeInventory() - DOESN'T EXIST + // player.getCursorItem() - DOESN'T EXIST + // player.kick(String) - DOESN'T EXIST - use packetHandler.disconnect() + + // Correct way to disconnect a player: + // playerRef.getPacketHandler().disconnect("Reason"); + }); + } +} diff --git a/app/src/main/java/org/example/docs/events/AsyncEventsExample.java b/app/src/main/java/org/example/docs/events/AsyncEventsExample.java new file mode 100644 index 0000000..a844915 --- /dev/null +++ b/app/src/main/java/org/example/docs/events/AsyncEventsExample.java @@ -0,0 +1,320 @@ +package org.example.docs.events; + +// Example from: hytale-docs/content/events/async-events.en.md +// This file tests that ALL async events documentation examples compile correctly + +import com.hypixel.hytale.event.EventPriority; +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.logger.HytaleLogger; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; + +/** + * Tests async-events.en.md documentation + * + * IMPORTANT NOTE: + * - PlayerChatEvent implements IAsyncEvent - it has a String key + * - Therefore, you MUST use registerAsyncGlobal() not registerAsync() + * - The simple registerAsync(Class, handler) only works for Void key events + */ +public class AsyncEventsExample { + + // ============================================ + // From "Registering Async Handlers" section + // CORRECTION: Must use registerAsyncGlobal() for PlayerChatEvent + // ============================================ + public void registerAsyncHandlers(EventRegistry eventRegistry) { + // PlayerChatEvent has String key - MUST use registerAsyncGlobal() + eventRegistry.registerAsyncGlobal(PlayerChatEvent.class, future -> { + return future.thenApply(event -> { + // This runs asynchronously + String filtered = filterContent(event.getContent()); + event.setContent(filtered); + return event; + }); + }); + } + + // ============================================ + // From "With Priority" section + // ============================================ + public void registerWithPriority(EventRegistry eventRegistry) { + eventRegistry.registerAsyncGlobal( + EventPriority.EARLY, + PlayerChatEvent.class, + future -> future.thenApply(event -> { + // Runs early in async chain + return event; + }) + ); + } + + // ============================================ + // From "Sequential Operations" section + // ============================================ + public void sequentialOperations(EventRegistry eventRegistry, HytaleLogger logger) { + eventRegistry.registerAsyncGlobal(PlayerChatEvent.class, future -> { + return future + .thenApply(event -> { + // Step 1: Filter content + event.setContent(filterProfanity(event.getContent())); + return event; + }) + .thenApply(event -> { + // Step 2: Add formatting + event.setContent(addChatFormatting(event.getContent())); + return event; + }) + .thenApply(event -> { + // Step 3: Log + logChatMessage(event.getSender(), event.getContent()); + return event; + }); + }); + } + + // ============================================ + // From "Parallel Operations" section + // ============================================ + public void parallelOperations(EventRegistry eventRegistry) { + eventRegistry.registerAsyncGlobal(PlayerChatEvent.class, future -> { + return future.thenCompose(event -> { + // Start multiple async operations in parallel + CompletableFuture spamCheckFuture = + CompletableFuture.supplyAsync(() -> checkForSpam(event.getContent())); + + CompletableFuture linkCheckFuture = + CompletableFuture.supplyAsync(() -> checkForLinks(event.getContent())); + + // Combine results + return spamCheckFuture.thenCombine(linkCheckFuture, (isSpam, hasLinks) -> { + if (isSpam || hasLinks) { + event.setCancelled(true); + } + return event; + }); + }); + }); + } + + // ============================================ + // From "Error Handling" section + // ============================================ + public void errorHandling(EventRegistry eventRegistry, HytaleLogger logger) { + eventRegistry.registerAsyncGlobal(PlayerChatEvent.class, future -> { + return future + .thenApply(event -> { + // May throw exception + riskyOperation(event); + return event; + }) + .exceptionally(throwable -> { + // Handle error + logger.at(Level.SEVERE).log("Async event failed: " + throwable.getMessage()); + return null; // Event will be skipped + }); + }); + } + + // ============================================ + // From "Switching to Main Thread" section + // ============================================ + public void switchToMainThread(EventRegistry eventRegistry) { + eventRegistry.registerAsyncGlobal(PlayerChatEvent.class, future -> { + return future.thenApply(event -> { + // Async: Check external database + boolean isMuted = checkMuteStatus(event.getSender().getUuid()); + + if (isMuted) { + event.setCancelled(true); + + // Schedule world thread work for player notification + World world = Universe.get().getWorld(event.getSender().getWorldUuid()); + if (world != null) { + world.execute(() -> { + event.getSender().sendMessage(Message.raw("You are muted!")); + }); + } + } + + return event; + }); + }); + } + + // ============================================ + // From "Complete Chat Handler Example" section + // ============================================ + public static class ChatPlugin extends JavaPlugin { + + public ChatPlugin(JavaPluginInit init) { + super(init); + } + + @Override + public void start() { + // Early priority: Content filtering + getEventRegistry().registerAsyncGlobal( + EventPriority.EARLY, + PlayerChatEvent.class, + this::filterContent + ); + + // Normal priority: Standard processing + getEventRegistry().registerAsyncGlobal( + PlayerChatEvent.class, + this::processChat + ); + + // Late priority: Logging + getEventRegistry().registerAsyncGlobal( + EventPriority.LATE, + PlayerChatEvent.class, + this::logChat + ); + } + + private CompletableFuture filterContent( + CompletableFuture future) { + return future.thenApply(event -> { + String content = event.getContent(); + + // Filter profanity + String filtered = filterProfanityStatic(content); + if (!filtered.equals(content)) { + event.setContent(filtered); + } + + return event; + }); + } + + private CompletableFuture processChat( + CompletableFuture future) { + return future.thenApply(event -> { + if (event.isCancelled()) { + return event; + } + + PlayerRef sender = event.getSender(); + + // Custom formatter with rank prefix + event.setFormatter((playerRef, msg) -> { + String prefix = getRankPrefix(playerRef); + return Message.raw(prefix + playerRef.getUsername() + ": " + msg); + }); + + return event; + }); + } + + private CompletableFuture logChat( + CompletableFuture future) { + return future.thenApply(event -> { + if (!event.isCancelled()) { + logToDatabase( + event.getSender().getUuid(), + event.getContent(), + System.currentTimeMillis() + ); + } + return event; + }); + } + + // Stubs for methods used in the example + private static String filterProfanityStatic(String content) { return content; } + private static String getRankPrefix(PlayerRef ref) { return "[Player] "; } + private static void logToDatabase(java.util.UUID uuid, String content, long time) {} + } + + // ============================================ + // From "Database Integration Example" section + // ============================================ + public static class ChatDatabasePlugin extends JavaPlugin { + + private final ExecutorService dbExecutor = Executors.newFixedThreadPool(4); + private Database database = null; // Stub + + public ChatDatabasePlugin(JavaPluginInit init) { + super(init); + } + + @Override + public void start() { + getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> { + return future.thenCompose(event -> { + // Run database check on dedicated thread pool + return CompletableFuture.supplyAsync(() -> { + try { + // Check if player is muted in database + boolean muted = database.isPlayerMuted(event.getSender().getUuid()); + if (muted) { + event.setCancelled(true); + } + return event; + } catch (Exception e) { + getLogger().at(Level.SEVERE).log("Database error: " + e.getMessage()); + return event; // Allow message on DB error + } + }, dbExecutor); + }); + }); + } + + @Override + public void shutdown() { + dbExecutor.shutdown(); + } + + // Stub interface + private interface Database { + boolean isPlayerMuted(java.util.UUID uuid); + } + } + + // ============================================ + // From "Best Practices" safe pattern section + // ============================================ + public void safePatternExample(EventRegistry eventRegistry) { + eventRegistry.registerAsyncGlobal(PlayerChatEvent.class, future -> { + return future.thenApply(event -> { + // Async work here... + String processed = processMessage(event.getContent()); + event.setContent(processed); + + // Safe player notification via PlayerRef + PlayerRef sender = event.getSender(); + World world = Universe.get().getWorld(sender.getWorldUuid()); + if (world != null) { + world.execute(() -> { + sender.sendMessage(Message.raw("Message processed")); + }); + } + + return event; + }); + }); + } + + // Stub methods used in examples + private String filterContent(String content) { return content; } + private String filterProfanity(String content) { return content; } + private String addChatFormatting(String content) { return content; } + private void logChatMessage(PlayerRef sender, String content) {} + private boolean checkForSpam(String content) { return false; } + private boolean checkForLinks(String content) { return false; } + private void riskyOperation(PlayerChatEvent event) {} + private boolean checkMuteStatus(java.util.UUID uuid) { return false; } + private String processMessage(String content) { return content; } +} diff --git a/app/src/main/java/org/example/docs/events/EventSystemExample.java b/app/src/main/java/org/example/docs/events/EventSystemExample.java new file mode 100644 index 0000000..6a2f48c --- /dev/null +++ b/app/src/main/java/org/example/docs/events/EventSystemExample.java @@ -0,0 +1,48 @@ +package org.example.docs.events; + +// Example from: hytale-docs/content/events/event-system.en.md +// This file tests that ALL events documentation examples compile correctly + +import com.hypixel.hytale.event.EventPriority; +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.logger.HytaleLogger; +import java.util.logging.Level; + +/** + * Tests event-system.en.md documentation + * + * IMPORTANT NOTE on Event Registration: + * - Events with Void key (like PlayerConnectEvent) can use register(Class, Consumer) + * - Events with other key types (like PlayerReadyEvent with String key) must use registerGlobal(Class, Consumer) + */ +public class EventSystemExample { + + public void registerEvents(EventRegistry eventRegistry, HytaleLogger logger) { + // PlayerConnectEvent has Void key, can use simple register() + eventRegistry.register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + logger.at(Level.INFO).log("Player connecting: " + playerRef.getUsername()); + }); + + // PlayerReadyEvent has String key - MUST use registerGlobal() + eventRegistry.registerGlobal(PlayerReadyEvent.class, event -> { + Player player = event.getPlayer(); + logger.at(Level.INFO).log("Player ready: " + player.getDisplayName()); + }); + + // PlayerDisconnectEvent - check its key type to determine which method to use + eventRegistry.registerGlobal(PlayerDisconnectEvent.class, event -> { + // Use event methods to access player info + }); + + // Register with specific priority (only works for Void key events) + eventRegistry.register(EventPriority.EARLY, PlayerConnectEvent.class, event -> { + // This runs before NORMAL priority handlers + }); + } +} diff --git a/app/src/main/java/org/example/docs/gettingstarted/CreatingPluginExample.java b/app/src/main/java/org/example/docs/gettingstarted/CreatingPluginExample.java new file mode 100644 index 0000000..85671bc --- /dev/null +++ b/app/src/main/java/org/example/docs/gettingstarted/CreatingPluginExample.java @@ -0,0 +1,56 @@ +package org.example.docs.gettingstarted; + +// Example from: hytale-docs/content/getting-started/creating-a-plugin.en.md +// This file tests that ALL code examples from the page compile correctly + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import java.util.logging.Level; + +/** + * Tests creating-a-plugin.en.md documentation + * + * From "Main Plugin Class" section (lines 85-118) + */ +public class CreatingPluginExample extends JavaPlugin { + + public CreatingPluginExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // Called during plugin initialization + // Register configs, prepare resources + getLogger().at(Level.INFO).log("MyPlugin is setting up!"); + } + + @Override + public void start() { + // Called when the plugin starts + // Register commands, events, entities + getLogger().at(Level.INFO).log("MyPlugin has started!"); + } + + @Override + public void shutdown() { + // Called when the plugin is stopping + // Clean up resources + getLogger().at(Level.INFO).log("MyPlugin is shutting down!"); + } + + // ============================================ + // From "Logger API" section (lines 129-138) + // ============================================ + public void testLoggerAPI() { + String playerName = "TestPlayer"; + + // Basic logging + getLogger().at(Level.INFO).log("Information message"); + getLogger().at(Level.WARNING).log("Warning message"); + getLogger().at(Level.SEVERE).log("Error message"); + + // With formatting + getLogger().at(Level.INFO).log("Player %s joined the game", playerName); + } +} diff --git a/app/src/main/java/org/example/docs/gettingstarted/PluginLifecycleExample.java b/app/src/main/java/org/example/docs/gettingstarted/PluginLifecycleExample.java new file mode 100644 index 0000000..f57c0cd --- /dev/null +++ b/app/src/main/java/org/example/docs/gettingstarted/PluginLifecycleExample.java @@ -0,0 +1,91 @@ +package org.example.docs.gettingstarted; + +// Example from: hytale-docs/content/getting-started/plugin-lifecycle.en.md +// This file tests that ALL code examples from the page compile correctly + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.plugin.PluginState; +import com.hypixel.hytale.server.core.util.Config; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import java.util.logging.Level; + +// Importing MyConfig from another example +import org.example.MyConfig; + +/** + * Tests plugin-lifecycle.en.md documentation + */ +public class PluginLifecycleExample extends JavaPlugin { + + private Config config; + + public PluginLifecycleExample(JavaPluginInit init) { + super(init); + } + + // ============================================ + // From "setup()" section (lines 37-45) + // ============================================ + @Override + public void setup() { + // Register a configuration file + config = withConfig(MyConfig.CODEC); + + getLogger().at(Level.INFO).log("Setup complete!"); + } + + // ============================================ + // From "start()" section (lines 66-85) + // Note: Fixed getCommandRegistry().register() -> registerCommand() + // Note: Fixed PlayerRef import path + // ============================================ + @Override + public void start() { + // Register commands + // getCommandRegistry().registerCommand(new MyCommand()); + + // Register events + getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect); + + getLogger().at(Level.INFO).log("Plugin started!"); + } + + private void onPlayerConnect(PlayerConnectEvent event) { + // See PlayerRef documentation for thread-safe player references + PlayerRef playerRef = event.getPlayerRef(); + getLogger().at(Level.INFO).log("Player connecting: " + playerRef.getUsername()); + } + + // ============================================ + // From "shutdown()" section (lines 101-109) + // ============================================ + @Override + public void shutdown() { + // Save any pending data + savePlayerData(); + + getLogger().at(Level.INFO).log("Plugin shutdown complete!"); + } + + private void savePlayerData() { + // Placeholder for save logic + } + + // ============================================ + // From "Checking Plugin State" section (lines 150-160) + // ============================================ + public void testPluginState() { + if (isEnabled()) { + // Plugin is running + } + + if (isDisabled()) { + // Plugin is not running + } + + // Get current state + PluginState state = getState(); + } +} diff --git a/app/src/main/java/org/example/docs/interactions/InteractionsIndexExample.java b/app/src/main/java/org/example/docs/interactions/InteractionsIndexExample.java new file mode 100644 index 0000000..94dbeb7 --- /dev/null +++ b/app/src/main/java/org/example/docs/interactions/InteractionsIndexExample.java @@ -0,0 +1,51 @@ +package org.example.docs.interactions; + +/** + * Example code from: hytale-docs/content/interactions/_index.en.md + * Tests the Interaction System documentation + */ + +import com.hypixel.hytale.assetstore.map.IndexedLookupTableAssetMap; +import com.hypixel.hytale.protocol.InteractionState; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction; + +public class InteractionsIndexExample { + + // CORRECTED Example from documentation - Get Interaction + // Documentation was WRONG: getAsset() takes int, not String! + public void getInteractionExample() { + // Get the asset map + IndexedLookupTableAssetMap assetMap = Interaction.getAssetMap(); + + // Get index from string ID + int index = assetMap.getIndex("my_interaction"); + + // Get interaction by index + Interaction interaction = assetMap.getAsset(index); + + // Or combined (but check for null!) + if (index != Integer.MIN_VALUE) { + Interaction myInteraction = assetMap.getAsset(index); + } + } + + // Example from documentation - InteractionState enum + // CORRECTED order: Finished(0), Skip(1), ItemChanged(2), Failed(3), NotFinished(4) + public void interactionStateExample() { + InteractionState state = InteractionState.NotFinished; + + // Check state + switch (state) { + case Finished: // value 0 + break; + case Skip: // value 1 + break; + case ItemChanged: // value 2 + break; + case Failed: // value 3 + break; + case NotFinished: // value 4 + break; + } + } +} diff --git a/app/src/main/java/org/example/docs/interactions/SelectorsExample.java b/app/src/main/java/org/example/docs/interactions/SelectorsExample.java new file mode 100644 index 0000000..fe5fbf3 --- /dev/null +++ b/app/src/main/java/org/example/docs/interactions/SelectorsExample.java @@ -0,0 +1,88 @@ +package org.example.docs.interactions; + +/** + * Example code from: hytale-docs/content/interactions/selectors.en.md + * Tests the Selectors documentation + */ + +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.selector.Selector; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class SelectorsExample { + + // Example from documentation - Select Nearby Blocks + public void selectNearbyBlocksExample(CommandBuffer commandBuffer, + Ref attackerRef, + double range) { + Selector.selectNearbyBlocks( + commandBuffer, + attackerRef, + range, + (x, y, z) -> { + // Process each block position + } + ); + } + + // Example from documentation - Select Nearby Blocks from position + public void selectNearbyBlocksFromPosition(Vector3d position, double range) { + Selector.selectNearbyBlocks( + position, + range, + (x, y, z) -> { + // Process each block position + } + ); + } + + // CORRECTED Example from documentation - Select Nearby Entities + // The consumer receives Ref only (not BiConsumer with Vector4d!) + public void selectNearbyEntitiesExample(CommandBuffer commandBuffer, + Ref attackerRef, + double range) { + Selector.selectNearbyEntities( + commandBuffer, + attackerRef, + range, + (Consumer>) entityRef -> { + // Process each entity + }, + (Predicate>) entityRef -> { + // Filter predicate - return true to include + return true; + } + ); + } + + // Example from documentation - Select Nearby Entities from position with ComponentAccessor + public void selectNearbyEntitiesFromPosition(ComponentAccessor componentAccessor, + Vector3d position, + double range) { + Selector.selectNearbyEntities( + componentAccessor, + position, + range, + entityRef -> { + // Process each entity + }, + entityRef -> true // Filter or null + ); + } + + // CORRECTED Example from documentation - Entity Validity Check + // Documentation was wrong: entityRef.get().isValid() should be entityRef.isValid() + public void entityValidityExample(Ref entityRef) { + // Correct way to check validity + if (entityRef.isValid()) { + // Safe to access entity components + // ... perform operations + } + } +} diff --git a/app/src/main/java/org/example/docs/inventory/ContainersExample.java b/app/src/main/java/org/example/docs/inventory/ContainersExample.java new file mode 100644 index 0000000..934481e --- /dev/null +++ b/app/src/main/java/org/example/docs/inventory/ContainersExample.java @@ -0,0 +1,499 @@ +package org.example.docs.inventory; + +// Example from: hytale-docs/content/inventory/containers.en.md +// Tests the Containers documentation examples + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.inventory.container.CombinedItemContainer; +import com.hypixel.hytale.server.core.inventory.container.SortType; +import com.hypixel.hytale.server.core.inventory.MaterialQuantity; +import com.hypixel.hytale.server.core.inventory.ResourceQuantity; +import com.hypixel.hytale.server.core.inventory.transaction.ItemStackTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ItemStackSlotTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.SlotTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.MoveTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ClearTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ListTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.MaterialTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ResourceTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.TagTransaction; +import com.hypixel.hytale.event.EventPriority; +import com.hypixel.hytale.protocol.SmartMoveType; + +import java.util.List; +import java.util.logging.Level; + +/** + * Tests Containers documentation + */ +public class ContainersExample extends JavaPlugin { + + public ContainersExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // All testing done in methods below + } + + // ============================================= + // Inventory Structure - CORRECT + // ============================================= + public void inventoryStructure(Player player) { + Inventory inv = player.getInventory(); + + // Get individual sections + ItemContainer storage = inv.getStorage(); // Main storage (36 slots default) + ItemContainer hotbar = inv.getHotbar(); // Hotbar (9 slots default) + ItemContainer armor = inv.getArmor(); // Armor slots + ItemContainer utility = inv.getUtility(); // Utility items (4 slots default) + ItemContainer backpack = inv.getBackpack(); // Backpack (expandable) + + // Combined containers for operations across multiple sections + CombinedItemContainer hotbarFirst = inv.getCombinedHotbarFirst(); + CombinedItemContainer storageFirst = inv.getCombinedStorageFirst(); + } + + // ============================================= + // ItemContainer Basics - Reading Items - CORRECT + // ============================================= + public void readingItems(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + // Get capacity (NOT getSize!) + short capacity = container.getCapacity(); + + // Get item at slot (slot is SHORT type) + ItemStack item = container.getItemStack((short) 0); + + // Check if empty + boolean empty = container.isEmpty(); + + // Safe item access + ItemStack stack = container.getItemStack((short) 0); + if (!ItemStack.isEmpty(stack)) { + String itemId = stack.getItemId(); + int quantity = stack.getQuantity(); + } + } + + // ============================================= + // Setting Items - CORRECT + // ============================================= + public void settingItems(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getHotbar(); + + // Set item at slot - returns a Transaction + ItemStackSlotTransaction transaction = container.setItemStackForSlot( + (short) 0, + new ItemStack("iron_sword", 1) + ); + + // Check if operation succeeded + if (transaction.succeeded()) { + // Item was set successfully + } + + // Clear a slot + container.setItemStackForSlot((short) 0, null); + } + + // ============================================= + // Adding Items - CORRECT + // ============================================= + public void addingItems(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + ItemStack toAdd = new ItemStack("wood_plank", 64); + + // Add to first available slot (stacks with existing items first) + ItemStackTransaction transaction = container.addItemStack(toAdd); + + // Check for remaining items that couldn't fit + ItemStack remainder = transaction.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + // Some items couldn't fit + int leftover = remainder.getQuantity(); + } + + // Add to specific slot + ItemStackSlotTransaction slotTransaction = container.addItemStackToSlot( + (short) 5, + toAdd + ); + + // Check if container can fit items before adding + if (container.canAddItemStack(toAdd)) { + container.addItemStack(toAdd); + } + } + + // ============================================= + // Removing Items - CORRECT + // ============================================= + public void removingItems(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + // Remove from specific slot + SlotTransaction transaction = container.removeItemStackFromSlot((short) 0); + + // Remove specific quantity from slot + ItemStackSlotTransaction removeTransaction = container.removeItemStackFromSlot( + (short) 0, + 10 // quantity to remove + ); + + // Remove specific item type from anywhere in container + ItemStack toRemove = new ItemStack("iron_ore", 5); + ItemStackTransaction itemTransaction = container.removeItemStack(toRemove); + + // Check if items can be removed before removing + if (container.canRemoveItemStack(toRemove)) { + container.removeItemStack(toRemove); + } + } + + // ============================================= + // Active Slots - CORRECT + // ============================================= + public void activeSlots(Player player) { + Inventory inv = player.getInventory(); + + // Get active hotbar slot (0-8) + byte activeHotbar = inv.getActiveHotbarSlot(); + + // Set active hotbar slot + inv.setActiveHotbarSlot((byte) 3); + + // Get item currently in hand + ItemStack handItem = inv.getItemInHand(); + + // Get/set active utility slot + byte activeUtility = inv.getActiveUtilitySlot(); + inv.setActiveUtilitySlot((byte) 1); + + // Get utility item + ItemStack utilityItem = inv.getUtilityItem(); + } + + // ============================================= + // Moving Items - CORRECT + // ============================================= + public void movingItems(Player player) { + Inventory inv = player.getInventory(); + ItemContainer storage = inv.getStorage(); + ItemContainer hotbar = inv.getHotbar(); + + // Move item from one slot to another in same container + MoveTransaction transaction = storage.moveItemStackFromSlotToSlot( + (short) 0, // from slot + 64, // quantity + storage, // to container + (short) 5 // to slot + ); + + // Move between different containers + storage.moveItemStackFromSlotToSlot( + (short) 0, + 32, + hotbar, + (short) 0 + ); + + // Use Inventory's moveItem for section-based moves + inv.moveItem( + Inventory.STORAGE_SECTION_ID, // from section + 5, // from slot + 10, // quantity + Inventory.HOTBAR_SECTION_ID, // to section + 0 // to slot + ); + } + + // ============================================= + // Smart Moving - CORRECT + // ============================================= + public void smartMoving(Player player) { + Inventory inv = player.getInventory(); + + // Smart move considers item type for optimal placement + inv.smartMoveItem( + Inventory.STORAGE_SECTION_ID, + 0, // slot + 64, // quantity + SmartMoveType.EquipOrMergeStack // tries to equip armor or merge stacks + ); + } + + // ============================================= + // Iterating Containers - CORRECT + // ============================================= + public void iteratingContainers(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + // Iterate all non-empty slots + container.forEach((slot, itemStack) -> { + getLogger().at(Level.INFO).log("Slot " + slot + ": " + + itemStack.getItemId() + " x" + itemStack.getQuantity()); + }); + + // Count items matching a condition + int swordCount = container.countItemStacks( + stack -> stack.getItemId().contains("sword") + ); + + // Check if container has stackable items + ItemStack testStack = new ItemStack("stone", 1); + boolean hasStackable = container.containsItemStacksStackableWith(testStack); + } + + // ============================================= + // Checking Contents - CORRECT + // ============================================= + public boolean hasItems(ItemContainer container, String itemId, int amount) { + int count = container.countItemStacks( + stack -> stack.getItemId().equals(itemId) + ); + return count >= amount; + } + + public short findSlotWithItem(ItemContainer container, String itemId) { + for (short i = 0; i < container.getCapacity(); i++) { + ItemStack stack = container.getItemStack(i); + if (!ItemStack.isEmpty(stack) && stack.getItemId().equals(itemId)) { + return i; + } + } + return -1; // Not found + } + + public int countEmptySlots(ItemContainer container) { + int empty = 0; + for (short i = 0; i < container.getCapacity(); i++) { + if (ItemStack.isEmpty(container.getItemStack(i))) { + empty++; + } + } + return empty; + } + + // ============================================= + // Clearing Containers - CORRECT + // ============================================= + public void clearingContainers(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + // Clear entire container + ClearTransaction transaction = container.clear(); + + // Drop all items (returns list of dropped items) + List droppedItems = container.dropAllItemStacks(); + + // Remove all items (returns list) + List removedItems = container.removeAllItemStacks(); + + // Clear entire player inventory + inv.clear(); + + // Drop all from player inventory + List allDropped = inv.dropAllItemStacks(); + } + + // ============================================= + // Sorting - CORRECT + // Available SortType values: NAME, TYPE, RARITY (no QUANTITY!) + // ============================================= + public void sorting(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + // Sort items + container.sortItems(SortType.NAME); + + // Sort via Inventory (also saves sort preference) + inv.sortStorage(SortType.NAME); + inv.setSortType(SortType.RARITY); // Or TYPE + } + + // ============================================= + // Container Events - CORRECT + // ============================================= + public void containerEvents(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + // Register for container change events + container.registerChangeEvent(event -> { + ItemContainer changedContainer = event.container(); + // Transaction transaction = event.transaction(); // Possible name collision + + getLogger().at(Level.INFO).log("Container changed!"); + }); + + // With priority + container.registerChangeEvent(EventPriority.EARLY, event -> { + // Handle early + }); + } + + // ============================================= + // Bulk Operations - CORRECT + // ============================================= + public void bulkOperations(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + // Add multiple items + List items = List.of( + new ItemStack("iron_ore", 10), + new ItemStack("gold_ore", 5) + ); + + if (container.canAddItemStacks(items)) { + ListTransaction transaction = container.addItemStacks(items); + } + + // Remove multiple items + if (container.canRemoveItemStacks(items)) { + container.removeItemStacks(items); + } + + // Add items in order (preserves slot positions) + container.addItemStacksOrdered(items); + container.addItemStacksOrdered((short) 5, items); // Starting at slot 5 + } + + // ============================================= + // Material and Resource Removal - CORRECT + // MaterialQuantity constructor: (itemId, resourceTypeId, tag, quantity, metadata) + // At least one of itemId, resourceTypeId, or tag must be non-null + // ============================================= + public void materialResourceRemoval(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + // Remove by material using itemId + MaterialQuantity materialByItem = new MaterialQuantity("iron_ingot", null, null, 5, null); + if (container.canRemoveMaterial(materialByItem)) { + MaterialTransaction transaction = container.removeMaterial(materialByItem); + } + + // Remove by material using resourceTypeId + MaterialQuantity materialByResource = new MaterialQuantity(null, "iron", null, 5, null); + if (container.canRemoveMaterial(materialByResource)) { + MaterialTransaction transaction = container.removeMaterial(materialByResource); + } + + // Remove by resource type (simpler constructor) + ResourceQuantity resource = new ResourceQuantity("wood", 10); + if (container.canRemoveResource(resource)) { + ResourceTransaction transaction = container.removeResource(resource); + } + + // Remove by tag index + int tagIndex = 0; + int quantity = 5; + if (container.canRemoveTag(tagIndex, quantity)) { + TagTransaction transaction = container.removeTag(tagIndex, quantity); + } + + // Bulk removal + List materials = List.of( + new MaterialQuantity("iron_ingot", null, null, 2, null), + new MaterialQuantity(null, null, "Wood", 5, null) // Using tag + ); + if (container.canRemoveMaterials(materials)) { + container.removeMaterials(materials); + } + } + + // ============================================= + // Advanced Move Operations - CORRECT + // ============================================= + public void advancedMoveOperations(Player player) { + Inventory inv = player.getInventory(); + ItemContainer storage = inv.getStorage(); + ItemContainer hotbar = inv.getHotbar(); + ItemContainer backpack = inv.getBackpack(); + + // Move all items to another container + ListTransaction> result = + storage.moveAllItemStacksTo(hotbar, backpack); + + // Move all items matching a condition + storage.moveAllItemStacksTo( + item -> item.getItemId().contains("ore"), + hotbar + ); + + // Quick stack: only moves items that can stack with existing items + storage.quickStackTo(hotbar); + + // Combine small stacks into one slot + storage.combineItemStacksIntoSlot(storage, (short) 0); + + // Swap items between containers + storage.swapItems( + (short) 0, // source position + hotbar, // target container + (short) 0, // destination position + (short) 5 // number of slots to swap + ); + } + + // ============================================= + // Replace Operations - CORRECT + // ============================================= + public void replaceOperations(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + ItemStack expectedItem = new ItemStack("iron_sword", 1); + ItemStack newItem = new ItemStack("diamond_sword", 1); + + // Replace item in slot if it matches expected + ItemStackSlotTransaction transaction = container.replaceItemStackInSlot( + (short) 0, + expectedItem, // must match current item to proceed + newItem + ); + + // Replace all items using a function + container.replaceAll((slot, existing) -> { + if (existing.getItemId().equals("old_item")) { + return new ItemStack("new_item", existing.getQuantity()); + } + return existing; + }); + } + + // ============================================= + // Safe pattern for working with containers - CORRECT + // ============================================= + public void safeAddItem(ItemContainer container, ItemStack item) { + if (ItemStack.isEmpty(item)) { + return; + } + + if (!container.canAddItemStack(item)) { + // Handle full container + return; + } + + ItemStackTransaction transaction = container.addItemStack(item); + if (!transaction.succeeded()) { + // Handle failure + } + } +} diff --git a/app/src/main/java/org/example/docs/inventory/InventoryExample.java b/app/src/main/java/org/example/docs/inventory/InventoryExample.java new file mode 100644 index 0000000..a63b02f --- /dev/null +++ b/app/src/main/java/org/example/docs/inventory/InventoryExample.java @@ -0,0 +1,212 @@ +package org.example.docs.inventory; + +// Example from: hytale-docs/content/inventory/_index.en.md and itemstacks.en.md +// Tests the inventory documentation examples +// +// FINDINGS: +// - player.getItemInHand() does NOT exist - use player.getInventory().getItemInHand() +// - item.getName() does NOT exist on Item - use item.getId() or stack.getItemId() +// - getQuantity() is correct (not getCount()) +// - withQuantity(0) returns null + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; + +import java.util.logging.Level; + +/** + * Tests inventory documentation + */ +public class InventoryExample extends JavaPlugin { + + public InventoryExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + Player player = event.getPlayer(); + + if (player != null) { + // ============================================= + // Getting Inventory - CORRECT + // ============================================= + Inventory inv = player.getInventory(); + + // ============================================= + // Getting Item in Hand - CORRECT pattern + // Note: player.getItemInHand() does NOT exist! + // Must use inventory method + // ============================================= + ItemStack hand = inv.getItemInHand(); + + // Check if item exists + if (hand != null && !hand.isEmpty()) { + // Item.getName() does NOT exist! + // Use getId() or getItemId() + String itemId = hand.getItemId(); + getLogger().at(Level.INFO).log("Holding: " + itemId); + + // Or via Item asset + Item item = hand.getItem(); + String id = item.getId(); + } + } + }); + } + + // ============================================= + // ItemStack Creation - CORRECT + // ============================================= + public void itemStackCreation() { + // Create by item ID + ItemStack sword = new ItemStack("iron_sword"); + + // Create with quantity + ItemStack materials = new ItemStack("wood_plank", 64); + + // Create with full parameters + ItemStack damagedSword = new ItemStack("iron_sword", 1, 50.0, 100.0, null); + } + + // ============================================= + // ItemStack Properties - CORRECT + // ============================================= + public void itemStackProperties() { + ItemStack stack = new ItemStack("iron_sword", 1); + + // Get the item ID + String itemId = stack.getItemId(); + + // Get the Item asset + Item item = stack.getItem(); + + // Get quantity (NOT getCount!) + int quantity = stack.getQuantity(); + + // Check if empty + boolean empty = stack.isEmpty(); + + // Check validity + boolean valid = stack.isValid(); + + // Durability + double durability = stack.getDurability(); + double maxDurability = stack.getMaxDurability(); + boolean unbreakable = stack.isUnbreakable(); + boolean broken = stack.isBroken(); + } + + // ============================================= + // Modifying ItemStacks - CORRECT (immutable pattern) + // ============================================= + public void modifyingItemStacks() { + ItemStack stack = new ItemStack("iron_sword", 1); + + // Change quantity - returns NEW ItemStack or null if quantity is 0 + ItemStack moreItems = stack.withQuantity(32); + + // Change durability + ItemStack damaged = stack.withDurability(50.0); + + // Increase durability + ItemStack repaired = stack.withIncreasedDurability(25.0); + + // Restore full durability + ItemStack fullyRepaired = stack.withRestoredDurability(100.0); + + // Change max durability + ItemStack stronger = stack.withMaxDurability(200.0); + + // Change state + ItemStack newState = stack.withState("activated"); + } + + // ============================================= + // EMPTY constant - CORRECT + // ============================================= + public void emptyStack() { + // Static empty instance (singleton) + ItemStack empty = ItemStack.EMPTY; + + // Static helper method (handles null and empty) + ItemStack stack = new ItemStack("iron_sword", 1); + if (ItemStack.isEmpty(stack)) { + // Handles null and empty stacks + } + } + + // ============================================= + // Comparing ItemStacks - CORRECT + // ============================================= + public void comparingItemStacks() { + ItemStack a = new ItemStack("iron_sword", 1); + ItemStack b = new ItemStack("iron_sword", 5); + ItemStack c = new ItemStack("diamond_sword", 1); + + // Check if stackable + boolean canStack = a.isStackableWith(b); + + // Check equivalent type + boolean sameType = a.isEquivalentType(b); + + // Check same item type only + boolean sameItem = ItemStack.isSameItemType(a, c); + + // Static helpers (handle nulls safely) + ItemStack.isStackableWith(a, b); + ItemStack.isEquivalentType(a, b); + } + + // ============================================= + // Working with Metadata - CORRECT + // ============================================= + public void metadataExample() { + ItemStack stack = new ItemStack("iron_sword", 1); + + // Create metadata codec + KeyedCodec OWNER_KEY = new KeyedCodec<>("Owner", Codec.STRING); + + // Add metadata + ItemStack withOwner = stack.withMetadata(OWNER_KEY, "PlayerName"); + + // Read metadata + String owner = withOwner.getFromMetadataOrNull(OWNER_KEY); + + // Read with key and codec + Integer level = stack.getFromMetadataOrNull("Level", Codec.INTEGER); + } + + // ============================================= + // Common Patterns - CORRECT + // ============================================= + public ItemStack consumeOne(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return null; + } + + int newQuantity = stack.getQuantity() - 1; + + // withQuantity returns null if quantity is 0 + return stack.withQuantity(newQuantity); + } + + public void useItem(ItemContainer container, short slot) { + ItemStack current = container.getItemStack(slot); + ItemStack remaining = consumeOne(current); + + // remaining may be null if stack was depleted + container.setItemStackForSlot(slot, remaining); + } +} diff --git a/app/src/main/java/org/example/docs/inventory/ItemStacksExample.java b/app/src/main/java/org/example/docs/inventory/ItemStacksExample.java new file mode 100644 index 0000000..d32f91a --- /dev/null +++ b/app/src/main/java/org/example/docs/inventory/ItemStacksExample.java @@ -0,0 +1,289 @@ +package org.example.docs.inventory; + +// Example from: hytale-docs/content/inventory/itemstacks.en.md +// Tests the ItemStacks documentation examples + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import org.bson.BsonDocument; + +/** + * Tests ItemStacks documentation + */ +public class ItemStacksExample extends JavaPlugin { + + public ItemStacksExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // All testing done in methods below + } + + // ============================================= + // Creating ItemStacks - CORRECT + // ============================================= + public void creatingItemStacks() { + // Create by item ID + ItemStack sword = new ItemStack("iron_sword"); + + // Create with quantity + ItemStack materials = new ItemStack("wood_plank", 64); + + // Create with quantity and metadata + BsonDocument metadata = new BsonDocument(); + ItemStack customItem = new ItemStack("iron_sword", 1, metadata); + + // Create with full parameters (durability) + ItemStack damagedSword = new ItemStack("iron_sword", 1, 50.0, 100.0, null); + // Parameters: itemId, quantity, durability, maxDurability, metadata + } + + // ============================================= + // ItemStack Properties - CORRECT + // ============================================= + public void itemStackProperties() { + ItemStack stack = new ItemStack("iron_sword", 1); + + // Get the item ID + String itemId = stack.getItemId(); // "iron_sword" + + // Get the Item asset + Item item = stack.getItem(); + + // Get quantity (NOT getCount!) + int quantity = stack.getQuantity(); + + // Check if empty + boolean empty = stack.isEmpty(); + + // Check validity + boolean valid = stack.isValid(); + + // Durability + double durability = stack.getDurability(); + double maxDurability = stack.getMaxDurability(); + boolean unbreakable = stack.isUnbreakable(); // true if maxDurability <= 0 + boolean broken = stack.isBroken(); // true if durability == 0 + } + + // ============================================= + // Modifying ItemStacks - CORRECT (immutable pattern) + // ============================================= + public void modifyingItemStacks() { + ItemStack stack = new ItemStack("iron_sword", 1); + + // Change quantity - returns NEW ItemStack or null if quantity is 0 + ItemStack moreItems = stack.withQuantity(32); + + // Change durability + ItemStack damaged = stack.withDurability(50.0); + + // Increase durability + ItemStack repaired = stack.withIncreasedDurability(25.0); + + // Restore full durability + ItemStack fullyRepaired = stack.withRestoredDurability(100.0); + + // Change max durability + ItemStack stronger = stack.withMaxDurability(200.0); + + // Change state (for items with states) + // ItemStack newState = stack.withState("activated"); + + // Add/modify metadata + BsonDocument metadataDocument = new BsonDocument(); + ItemStack withMeta = stack.withMetadata(metadataDocument); + + // Add specific metadata value + ItemStack tagged = stack.withMetadata("CustomKey", Codec.STRING, "CustomValue"); + } + + // ============================================= + // The EMPTY Constant - CORRECT + // ============================================= + public void emptyConstant() { + // Static empty instance (singleton) + ItemStack empty = ItemStack.EMPTY; + + ItemStack stack = new ItemStack("iron_sword", 1); + // Check for empty + if (stack.isEmpty()) { + // Stack is empty + } + + // Static helper method + if (ItemStack.isEmpty(stack)) { + // Handles null and empty stacks + } + } + + // ============================================= + // Comparing ItemStacks - CORRECT + // ============================================= + public void comparingItemStacks() { + ItemStack a = new ItemStack("iron_sword", 1); + ItemStack b = new ItemStack("iron_sword", 5); + ItemStack c = new ItemStack("diamond_sword", 1); + + // Check if stackable (same itemId, durability, maxDurability, AND metadata) + // Note: Different quantities can stack, but durability values must match exactly + boolean canStack = a.isStackableWith(b); + + // Check equivalent type (same itemId and metadata, ignores durability values) + boolean sameType = a.isEquivalentType(b); + + // Check same item type only (just itemId comparison) + boolean sameItem = ItemStack.isSameItemType(a, c); // false + + // Static helpers (handle nulls safely) + ItemStack.isStackableWith(a, b); + ItemStack.isEquivalentType(a, b); + } + + // ============================================= + // Working with Metadata - CORRECT + // ============================================= + public void metadataExample() { + ItemStack stack = new ItemStack("iron_sword", 1); + + // Create metadata codec + KeyedCodec OWNER_KEY = new KeyedCodec<>("Owner", Codec.STRING); + + // Add metadata + ItemStack withOwner = stack.withMetadata(OWNER_KEY, "PlayerName"); + + // Read metadata + String owner = withOwner.getFromMetadataOrNull(OWNER_KEY); + + // Read with key and codec + Integer level = stack.getFromMetadataOrNull("Level", Codec.INTEGER); + } + + // ============================================= + // Block Items - CORRECT + // ============================================= + public void blockItems() { + ItemStack stack = new ItemStack("stone", 1); + + // Get associated block key (null if not a block item) + String blockKey = stack.getBlockKey(); + if (blockKey != null) { + // This item can be placed as a block + } + + // Check via Item asset + Item item = stack.getItem(); + if (item.hasBlockType()) { + String blockId = item.getBlockId(); + } + } + + // ============================================= + // Common Patterns - CORRECT + // ============================================= + + // Consuming Items + public ItemStack consumeOne(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return null; + } + + int newQuantity = stack.getQuantity() - 1; + + // withQuantity returns null if quantity is 0 + return stack.withQuantity(newQuantity); + } + + // Usage with container + public void useItem(ItemContainer container, short slot) { + ItemStack current = container.getItemStack(slot); + ItemStack remaining = consumeOne(current); + + // remaining may be null if stack was depleted + container.setItemStackForSlot(slot, remaining); + } + + // Checking Item Type + public boolean isHoldingSword(ItemStack hand) { + if (hand == null || hand.isEmpty()) { + return false; + } + + // Check item type by ID + return hand.getItemId().contains("sword"); + + // Or check via Item asset + // return hand.getItem().getCategory().equals("weapon"); + } + + // Splitting Stacks + public ItemStack[] splitStack(ItemStack stack, int splitAmount) { + if (stack == null || stack.isEmpty()) { + return null; + } + + int currentQuantity = stack.getQuantity(); + if (splitAmount >= currentQuantity) { + return new ItemStack[] { stack, null }; + } + + // Create two stacks + ItemStack remaining = stack.withQuantity(currentQuantity - splitAmount); + ItemStack split = stack.withQuantity(splitAmount); + + return new ItemStack[] { remaining, split }; + } + + // Merging Stacks + public ItemStack[] mergeStacks(ItemStack target, ItemStack source) { + if (!target.isStackableWith(source)) { + return new ItemStack[] { target, source }; // Can't merge + } + + int maxStack = target.getItem().getMaxStack(); + int totalQuantity = target.getQuantity() + source.getQuantity(); + + if (totalQuantity <= maxStack) { + // Full merge + return new ItemStack[] { + target.withQuantity(totalQuantity), + null + }; + } + + // Partial merge + return new ItemStack[] { + target.withQuantity(maxStack), + source.withQuantity(totalQuantity - maxStack) + }; + } + + // ============================================= + // Best Practices - CORRECT + // ============================================= + public void bestPractices() { + ItemStack stack = new ItemStack("iron_sword", 1); + int newQuantity = 10; + + // Good: Handle null from withQuantity + ItemStack result = stack.withQuantity(newQuantity); + if (result == null) { + // Stack depleted, handle appropriately + } + + // Good: Safe empty check + if (ItemStack.isEmpty(stack)) { + // Handles both null and empty ItemStack.EMPTY + } + + // Bad: Ignoring the returned value + stack.withQuantity(10); // Returns new stack, original unchanged! + } +} diff --git a/app/src/main/java/org/example/docs/inventory/TransactionsExample.java b/app/src/main/java/org/example/docs/inventory/TransactionsExample.java new file mode 100644 index 0000000..a1dfce4 --- /dev/null +++ b/app/src/main/java/org/example/docs/inventory/TransactionsExample.java @@ -0,0 +1,362 @@ +package org.example.docs.inventory; + +// Example from: hytale-docs/content/inventory/transactions.en.md +// Tests the Transactions documentation examples + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.inventory.transaction.Transaction; +import com.hypixel.hytale.server.core.inventory.transaction.SlotTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ItemStackSlotTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ItemStackTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.MoveTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ListTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ActionType; +import com.hypixel.hytale.server.core.inventory.transaction.MoveType; +import com.hypixel.hytale.server.core.Message; + +import java.util.List; +import java.util.logging.Level; + +/** + * Tests Transactions documentation + */ +public class TransactionsExample extends JavaPlugin { + + public TransactionsExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // All testing done in methods below + } + + // ============================================= + // SlotTransaction - CORRECT + // ============================================= + public void slotTransactionExample(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + SlotTransaction transaction = container.removeItemStackFromSlot((short) 0); + + if (transaction.succeeded()) { + // What was in the slot before + ItemStack before = transaction.getSlotBefore(); + + // What is in the slot now + ItemStack after = transaction.getSlotAfter(); + + // The item that was removed/output + ItemStack output = transaction.getOutput(); + + // Which slot was affected + short slot = transaction.getSlot(); + + // What type of action (ADD, REMOVE, REPLACE) + ActionType action = transaction.getAction(); + } + } + + // ============================================= + // ItemStackSlotTransaction - CORRECT + // ============================================= + public void itemStackSlotTransactionExample(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + ItemStackSlotTransaction transaction = container.setItemStackForSlot( + (short) 0, + new ItemStack("iron_sword", 1) + ); + + if (transaction.succeeded()) { + short slot = transaction.getSlot(); + ItemStack before = transaction.getSlotBefore(); + ItemStack after = transaction.getSlotAfter(); + + // Check options used + boolean wasFiltered = transaction.isFilter(); + boolean wasAllOrNothing = transaction.isAllOrNothing(); + } + } + + // ============================================= + // ItemStackTransaction - CORRECT + // ============================================= + public void itemStackTransactionExample(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + ItemStack toAdd = new ItemStack("stone", 128); + ItemStackTransaction transaction = container.addItemStack(toAdd); + + if (transaction.succeeded()) { + // Items that couldn't fit (null if all fit) + ItemStack remainder = transaction.getRemainder(); + + // Original item we tried to add + ItemStack query = transaction.getQuery(); + + // List of all slot transactions that occurred + List slotTransactions = transaction.getSlotTransactions(); + + for (ItemStackSlotTransaction slotTx : slotTransactions) { + getLogger().at(Level.INFO).log("Modified slot " + slotTx.getSlot()); + } + } + } + + // ============================================= + // MoveTransaction - CORRECT + // ============================================= + public void moveTransactionExample(Player player) { + Inventory inv = player.getInventory(); + ItemContainer storage = inv.getStorage(); + ItemContainer hotbar = inv.getHotbar(); + + MoveTransaction transaction = storage.moveItemStackFromSlotToSlot( + (short) 0, + 32, + hotbar, + (short) 0 + ); + + if (transaction.succeeded()) { + // Transaction for removing from source + SlotTransaction removeTransaction = transaction.getRemoveTransaction(); + + // Transaction for adding to destination + SlotTransaction addTransaction = transaction.getAddTransaction(); + + // The destination container + ItemContainer destination = transaction.getOtherContainer(); + + // Direction of the move + MoveType moveType = transaction.getMoveType(); + } + } + + // ============================================= + // ListTransaction - CORRECT + // ============================================= + public void listTransactionExample(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + List items = List.of( + new ItemStack("stone", 64), + new ItemStack("wood", 64) + ); + + ListTransaction transaction = container.addItemStacks(items); + + if (transaction.succeeded()) { + List results = transaction.getList(); + + for (ItemStackTransaction result : results) { + if (result.succeeded()) { + ItemStack remainder = result.getRemainder(); + // ... + } + } + } + } + + // ============================================= + // ActionType - CORRECT + // ============================================= + public void actionTypeExample(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + + SlotTransaction transaction = container.removeItemStackFromSlot((short) 0); + if (transaction.succeeded()) { + ActionType action = transaction.getAction(); + + // Check action characteristics + if (action.isAdd()) { /* operation adds items */ } + if (action.isRemove()) { /* operation removes items */ } + if (action.isDestroy()) { /* operation destroys slot contents */ } + } + } + + // ============================================= + // MoveType - CORRECT + // ============================================= + public void moveTypeExample(Player player) { + Inventory inv = player.getInventory(); + ItemContainer storage = inv.getStorage(); + ItemContainer hotbar = inv.getHotbar(); + + MoveTransaction transaction = storage.moveItemStackFromSlotToSlot( + (short) 0, + 32, + hotbar, + (short) 0 + ); + + if (transaction.succeeded()) { + MoveType moveType = transaction.getMoveType(); + // MOVE_TO_SELF: Items being moved TO this container + // MOVE_FROM_SELF: Items being moved FROM this container + } + } + + // ============================================= + // Common Patterns - Check Before Modify - CORRECT + // ============================================= + public boolean safeTransfer(ItemContainer from, ItemContainer to, String itemId, int amount) { + ItemStack toRemove = new ItemStack(itemId, amount); + + // Check both operations can succeed + if (!from.canRemoveItemStack(toRemove)) { + return false; // Not enough items + } + + if (!to.canAddItemStack(toRemove)) { + return false; // No space + } + + // Execute removal + ItemStackTransaction removeResult = from.removeItemStack(toRemove); + if (!removeResult.succeeded()) { + return false; + } + + // Execute addition + ItemStack removed = removeResult.getQuery(); + ItemStackTransaction addResult = to.addItemStack(removed); + + // Handle any remainder + ItemStack remainder = addResult.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + // Put remainder back + from.addItemStack(remainder); + } + + return true; + } + + // ============================================= + // Handling Remainders - CORRECT + // ============================================= + public void giveItemSafe(Player player, String itemId, int quantity) { + ItemContainer storage = player.getInventory().getStorage(); + ItemStack item = new ItemStack(itemId, quantity); + + ItemStackTransaction result = storage.addItemStack(item); + + if (!result.succeeded()) { + player.sendMessage(Message.raw("Inventory full!")); + return; + } + + ItemStack remainder = result.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + player.sendMessage(Message.raw( + "Only received " + (quantity - remainder.getQuantity()) + " items, " + + "inventory full!" + )); + } + } + + // ============================================= + // Atomic Operations - CORRECT + // ============================================= + public boolean buyItem(Player player, String itemId, int price, int quantity) { + Inventory inv = player.getInventory(); + ItemContainer storage = inv.getStorage(); + + ItemStack currency = new ItemStack("gold_coin", price); + ItemStack item = new ItemStack(itemId, quantity); + + // Check both operations can succeed fully + if (!storage.canRemoveItemStack(currency)) { + player.sendMessage(Message.raw("Not enough gold!")); + return false; + } + + if (!storage.canAddItemStack(item)) { + player.sendMessage(Message.raw("Inventory full!")); + return false; + } + + // Remove currency with allOrNothing=true + ItemStackTransaction removeResult = storage.removeItemStack(currency, true, true); + if (!removeResult.succeeded()) { + return false; + } + + // Add item + ItemStackTransaction addResult = storage.addItemStack(item, true, false, true); + if (!addResult.succeeded()) { + // Rollback: return the currency + storage.addItemStack(currency); + return false; + } + + return true; + } + + // ============================================= + // Tracking Changes - CORRECT + // ============================================= + public void onContainerChange(ItemContainer.ItemContainerChangeEvent event) { + Transaction transaction = event.transaction(); + + // Check if hotbar slot 0 was affected + if (transaction.wasSlotModified((short) 0)) { + getLogger().at(Level.INFO).log("First slot was modified!"); + } + + // Check all hotbar slots + for (short i = 0; i < 9; i++) { + if (transaction.wasSlotModified(i)) { + getLogger().at(Level.INFO).log("Hotbar slot " + i + " modified"); + } + } + } + + // ============================================= + // Transaction Options - CORRECT + // ============================================= + public void transactionOptions(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + ItemStack item = new ItemStack("stone", 64); + + // Default behavior + container.addItemStack(item); + + // With options + container.addItemStack(item, true, false, true); // allOrNothing, fullStacks, filter + container.removeItemStack(item, true, true); // allOrNothing, filter + } + + // ============================================= + // Best Practices - CORRECT + // ============================================= + public void bestPractices(Player player) { + Inventory inv = player.getInventory(); + ItemContainer container = inv.getStorage(); + ItemStack item = new ItemStack("stone", 64); + + // Good: Check success and handle remainder + ItemStackTransaction tx = container.addItemStack(item); + if (tx.succeeded()) { + ItemStack remainder = tx.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + // Handle leftover items + } + } + + // Bad: Assume success + container.addItemStack(item); // Might fail silently! + } +} diff --git a/app/src/main/java/org/example/docs/permissions/PermissionsExample.java b/app/src/main/java/org/example/docs/permissions/PermissionsExample.java new file mode 100644 index 0000000..9ac589a --- /dev/null +++ b/app/src/main/java/org/example/docs/permissions/PermissionsExample.java @@ -0,0 +1,301 @@ +package org.example.docs.permissions; + +// Example from: hytale-docs/content/permissions/_index.en.md +// Tests the Permissions documentation examples +// +// FINDINGS: +// - HytaleServer.get().getPermissionsModule() does NOT exist - use PermissionsModule.get() +// - PlayerGroupEvent.isAdded() does NOT exist - use PlayerGroupEvent.Added or PlayerGroupEvent.Removed +// - PlayerGroupEvent.getGroup() does NOT exist - use getGroupName() +// - GroupPermissionChangeEvent uses separate classes: Added and Removed +// - PermissionProvider interface does NOT have hasPermission(UUID, String) +// - The interface has: getUserPermissions(), getGroupPermissions(), getGroupsForUser(), etc. + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.permissions.PermissionsModule; +import com.hypixel.hytale.server.core.permissions.provider.PermissionProvider; +import com.hypixel.hytale.server.core.event.events.permissions.PlayerGroupEvent; +import com.hypixel.hytale.server.core.event.events.permissions.GroupPermissionChangeEvent; +import com.hypixel.hytale.server.core.event.events.permissions.PlayerPermissionChangeEvent; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; + +import java.util.Set; +import java.util.UUID; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import javax.annotation.Nonnull; + +/** + * Tests Permissions documentation + */ +public class PermissionsExample extends JavaPlugin { + + public PermissionsExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // Register permission events + registerEvents(); + } + + @Override + protected void start() { + // Add custom permission provider + PermissionsModule permissions = PermissionsModule.get(); + permissions.addProvider(new VIPPermissionProvider()); + } + + // ============================================= + // Accessing Permissions Module - CORRECT + // Note: HytaleServer.get().getPermissionsModule() does NOT exist! + // ============================================= + public void accessPermissionsModule() { + // CORRECT way to access + PermissionsModule permissions = PermissionsModule.get(); + + // WRONG - this method does not exist: + // PermissionsModule permissions = HytaleServer.get().getPermissionsModule(); + } + + // ============================================= + // Checking Permissions - CORRECT + // ============================================= + public void checkingPermissions(Player player, CommandContext ctx) { + PermissionsModule permissions = PermissionsModule.get(); + UUID playerUuid = player.getPlayerRef().getUuid(); + + // Check if player has permission + boolean canEdit = permissions.hasPermission(playerUuid, "hytale.editor.asset"); + + // Check with default value + boolean canBuild = permissions.hasPermission(playerUuid, "myplugin.build", false); + + // Check in command context + if (permissions.hasPermission(ctx.sender().getUuid(), "myplugin.admin")) { + // Admin-only logic + } + } + + // ============================================= + // User Permissions - CORRECT + // ============================================= + public void userPermissions(Player player) { + PermissionsModule permissions = PermissionsModule.get(); + UUID playerUuid = player.getPlayerRef().getUuid(); + + // Add permissions + permissions.addUserPermission(playerUuid, Set.of( + "myplugin.feature.create", + "myplugin.feature.delete" + )); + + // Remove permissions + permissions.removeUserPermission(playerUuid, Set.of( + "myplugin.feature.delete" + )); + } + + // ============================================= + // Group Permissions - CORRECT + // ============================================= + public void groupPermissions(Player player) { + PermissionsModule permissions = PermissionsModule.get(); + UUID playerUuid = player.getPlayerRef().getUuid(); + + // Add player to a group + permissions.addUserToGroup(playerUuid, "moderators"); + + // Remove player from a group + permissions.removeUserFromGroup(playerUuid, "moderators"); + + // Get all groups for a player + Set groups = permissions.getGroupsForUser(playerUuid); + + // Add permissions to a group + permissions.addGroupPermission("moderators", Set.of( + "server.kick", + "server.mute" + )); + + // Remove permissions from a group + permissions.removeGroupPermission("moderators", Set.of( + "server.mute" + )); + } + + // ============================================= + // Permission Events - CORRECT + // Note: Events use subclasses like PlayerGroupEvent.Added + // ============================================= + public void registerEvents() { + // Player ADDED to group + getEventRegistry().register(PlayerGroupEvent.Added.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + String group = event.getGroupName(); // NOT getGroup()! + getLogger().at(Level.INFO).log("Player joined group: " + group); + }); + + // Player REMOVED from group + getEventRegistry().register(PlayerGroupEvent.Removed.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + String group = event.getGroupName(); + getLogger().at(Level.INFO).log("Player left group: " + group); + }); + + // Group permissions ADDED + getEventRegistry().register(GroupPermissionChangeEvent.Added.class, event -> { + String group = event.getGroupName(); + Set added = event.getAddedPermissions(); + getLogger().at(Level.INFO).log("Group " + group + " got permissions: " + added); + }); + + // Group permissions REMOVED + getEventRegistry().register(GroupPermissionChangeEvent.Removed.class, event -> { + String group = event.getGroupName(); + Set removed = event.getRemovedPermissions(); + getLogger().at(Level.INFO).log("Group " + group + " lost permissions: " + removed); + }); + + // Player permissions ADDED + getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsAdded.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + Set added = event.getAddedPermissions(); + getLogger().at(Level.INFO).log("Player got permissions: " + added); + }); + + // Player permissions REMOVED + getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsRemoved.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + Set removed = event.getRemovedPermissions(); + getLogger().at(Level.INFO).log("Player lost permissions: " + removed); + }); + } + + // ============================================= + // Custom Permission Provider - CORRECT interface + // Note: PermissionProvider does NOT have hasPermission(UUID, String)! + // ============================================= + public static class VIPPermissionProvider implements PermissionProvider { + private final Map> vipPermissions = new ConcurrentHashMap<>(); + private final Map> vipGroups = new ConcurrentHashMap<>(); + + @Override + @Nonnull + public String getName() { + return "VIPPermissionProvider"; + } + + @Override + public void addUserPermissions(@Nonnull UUID uuid, @Nonnull Set permissions) { + vipPermissions.computeIfAbsent(uuid, k -> new HashSet<>()).addAll(permissions); + } + + @Override + public void removeUserPermissions(@Nonnull UUID uuid, @Nonnull Set permissions) { + Set userPerms = vipPermissions.get(uuid); + if (userPerms != null) { + userPerms.removeAll(permissions); + } + } + + @Override + public Set getUserPermissions(@Nonnull UUID uuid) { + return vipPermissions.getOrDefault(uuid, Collections.emptySet()); + } + + @Override + public void addGroupPermissions(@Nonnull String group, @Nonnull Set permissions) { + // Not implemented in this example + } + + @Override + public void removeGroupPermissions(@Nonnull String group, @Nonnull Set permissions) { + // Not implemented in this example + } + + @Override + public Set getGroupPermissions(@Nonnull String group) { + return Collections.emptySet(); + } + + @Override + public void addUserToGroup(@Nonnull UUID uuid, @Nonnull String group) { + vipGroups.computeIfAbsent(uuid, k -> new HashSet<>()).add(group); + } + + @Override + public void removeUserFromGroup(@Nonnull UUID uuid, @Nonnull String group) { + Set groups = vipGroups.get(uuid); + if (groups != null) { + groups.remove(group); + } + } + + @Override + public Set getGroupsForUser(@Nonnull UUID uuid) { + return vipGroups.getOrDefault(uuid, Collections.emptySet()); + } + } + + // ============================================= + // Role-Based Access Example - CORRECT + // ============================================= + public class RoleManager { + private final PermissionsModule permissions; + + public RoleManager() { + this.permissions = PermissionsModule.get(); + } + + public void promoteToModerator(UUID playerUuid) { + permissions.addUserToGroup(playerUuid, "moderators"); + permissions.addUserPermission(playerUuid, Set.of( + "server.kick", + "server.mute", + "server.warn" + )); + } + + public void demoteFromModerator(UUID playerUuid) { + permissions.removeUserFromGroup(playerUuid, "moderators"); + permissions.removeUserPermission(playerUuid, Set.of( + "server.kick", + "server.mute", + "server.warn" + )); + } + + public boolean canModerate(UUID playerUuid) { + return permissions.getGroupsForUser(playerUuid).contains("moderators"); + } + } + + // ============================================= + // Feature Gating Example - CORRECT + // ============================================= + public void useFeature(Player player, String featureName) { + UUID uuid = player.getPlayerRef().getUuid(); + PermissionsModule perms = PermissionsModule.get(); + + String permission = "myplugin.feature." + featureName; + if (!perms.hasPermission(uuid, permission)) { + player.sendMessage(Message.translation("error.no_permission")); + return; + } + + // Execute feature + executeFeature(player, featureName); + } + + private void executeFeature(Player player, String featureName) { + // Feature implementation + } +} diff --git a/app/src/main/java/org/example/docs/reference/AllRegistriesExample.java b/app/src/main/java/org/example/docs/reference/AllRegistriesExample.java new file mode 100644 index 0000000..153d2ea --- /dev/null +++ b/app/src/main/java/org/example/docs/reference/AllRegistriesExample.java @@ -0,0 +1,69 @@ +package org.example.docs.reference; + +/** + * Example code from: hytale-docs/content/reference/all-registries.en.md + * Tests the All Registries documentation + */ + +import com.hypixel.hytale.assetstore.map.DefaultAssetMap; +import com.hypixel.hytale.event.EventPriority; +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.universe.PlayerRef; + +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +public class AllRegistriesExample { + + // Placeholder - cannot extend JavaPlugin directly in test + private EventRegistry events; + private com.hypixel.hytale.server.core.command.system.CommandRegistry commands; + private java.util.logging.Logger logger; + + // Example from documentation - Event Registration + public void eventRegistryExample() { + // Use getPlayerRef() - getPlayer() is deprecated + events.register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + playerRef.sendMessage(Message.raw("Welcome!")); + }); + + events.register(EventPriority.FIRST, PlayerDisconnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + logger.log(Level.INFO, playerRef.getUsername() + " left"); + }); + } + + // Example from documentation - Command Registration + // AbstractCommand.execute() returns CompletableFuture + public void commandRegistryExample() { + commands.registerCommand(new AbstractCommand("hello", "my_plugin.commands.hello.description") { + @Override + protected CompletableFuture execute(CommandContext ctx) { + ctx.sender().sendMessage(Message.raw("Hello, World!")); + return null; // Return null for synchronous completion + } + }); + } + + // CORRECTED Example from documentation - Accessing Assets + // Item.getAssetMap() returns DefaultAssetMap which takes String keys directly + // (Unlike Interaction which uses IndexedLookupTableAssetMap with int indices) + public void accessingAssetsExample() { + // Get items by string ID directly + DefaultAssetMap itemMap = Item.getAssetMap(); + Item sword = itemMap.getAsset("iron_sword"); + + // Get block types - also uses string key directly + DefaultAssetMap blockMap = BlockType.getAssetMap(); + BlockType stone = blockMap.getAsset("stone"); + } +} diff --git a/app/src/main/java/org/example/docs/tasks/TasksExample.java b/app/src/main/java/org/example/docs/tasks/TasksExample.java new file mode 100644 index 0000000..c9c23cb --- /dev/null +++ b/app/src/main/java/org/example/docs/tasks/TasksExample.java @@ -0,0 +1,244 @@ +package org.example.docs.tasks; + +// Example from: hytale-docs/content/tasks/_index.en.md, task-registry.en.md, async-operations.en.md +// Tests the Tasks documentation examples +// +// FINDINGS: +// - TaskRegistry API is correctly documented: registerTask(CompletableFuture) and registerTask(ScheduledFuture) +// - Documentation correctly warns that runAsync(), runLater(), runRepeating() do NOT exist +// - Standard Java concurrency (CompletableFuture, ScheduledExecutorService) is the correct approach + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.task.TaskRegistration; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; + +/** + * Tests Tasks documentation + */ +public class TasksExample extends JavaPlugin { + + private ScheduledExecutorService scheduler; + + public TasksExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // Register player connect event for async data loading demo + getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + World world = event.getWorld(); + loadPlayerData(playerRef, world); + }); + } + + @Override + protected void start() { + // Create scheduler for repeating tasks + scheduler = Executors.newSingleThreadScheduledExecutor(); + + // Schedule auto-save every 5 minutes + @SuppressWarnings("unchecked") + ScheduledFuture saveTask = (ScheduledFuture) scheduler.scheduleAtFixedRate( + () -> { + saveAllData(); + getLogger().at(Level.INFO).log("Auto-save complete"); + }, + 5, // Initial delay + 5, // Period + TimeUnit.MINUTES + ); + + // Register for tracking + getTaskRegistry().registerTask(saveTask); + } + + @Override + protected void shutdown() { + if (scheduler != null) { + scheduler.shutdown(); + } + } + + // ============================================= + // Basic Async Pattern - CORRECT + // ============================================= + public void basicAsyncExample(Player player) { + PlayerRef playerRef = player.getPlayerRef(); + World world = player.getWorld(); + + CompletableFuture task = CompletableFuture.runAsync(() -> { + // This runs on a background thread + // Do heavy work here + String result = "Heavy computation result"; + + // Return to world thread for game state changes + world.execute(() -> { + playerRef.sendMessage(Message.raw("Result: " + result)); + }); + }); + + // Register with TaskRegistry for tracking + getTaskRegistry().registerTask(task); + } + + // ============================================= + // Delayed Task - CORRECT + // ============================================= + public void delayedTaskExample(PlayerRef playerRef, World world) { + // Run after 3 seconds + CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS) + .execute(() -> { + world.execute(() -> { + playerRef.sendMessage(Message.raw("3 seconds have passed!")); + }); + }); + } + + // ============================================= + // Async Data Loading - CORRECT + // ============================================= + public void loadPlayerData(PlayerRef playerRef, World world) { + CompletableFuture.runAsync(() -> { + // Load from database (blocking I/O is OK here) + String data = simulateDatabaseLoad(playerRef.getUuid()); + + // Return to world thread for game state changes + world.execute(() -> { + Ref ref = playerRef.getReference(); + if (ref != null && ref.isValid()) { + playerRef.sendMessage(Message.raw("Data loaded: " + data)); + } + }); + }); + } + + // ============================================= + // Countdown Timer - CORRECT + // ============================================= + public void startCountdown(PlayerRef playerRef, int seconds) { + World world = Universe.get().getWorld(playerRef.getWorldUuid()); + if (world == null) return; + + AtomicInteger remaining = new AtomicInteger(seconds); + + ScheduledExecutorService countdownScheduler = Executors.newSingleThreadScheduledExecutor(); + countdownScheduler.scheduleAtFixedRate(() -> { + int count = remaining.decrementAndGet(); + + world.execute(() -> { + if (count > 0) { + playerRef.sendMessage(Message.raw("Starting in: " + count)); + } else { + playerRef.sendMessage(Message.raw("Go!")); + countdownScheduler.shutdown(); + } + }); + }, 0, 1, TimeUnit.SECONDS); + } + + // ============================================= + // Thread-Safe Collections - CORRECT + // ============================================= + private final Map playerData = new ConcurrentHashMap<>(); + private final java.util.Set processing = ConcurrentHashMap.newKeySet(); + + public void processPlayer(PlayerRef playerRef, World world) { + UUID uuid = playerRef.getUuid(); + + // Prevent duplicate processing + if (!processing.add(uuid)) { + return; // Already processing + } + + CompletableFuture.runAsync(() -> { + try { + String data = simulateHeavyComputation(uuid); + playerData.put(uuid, data); + + world.execute(() -> { + playerRef.sendMessage(Message.raw("Processing complete!")); + }); + } finally { + processing.remove(uuid); + } + }); + } + + // ============================================= + // Error Handling - CORRECT + // ============================================= + public void safeAsyncOperation(PlayerRef playerRef, World world) { + CompletableFuture.runAsync(() -> { + try { + riskyOperation(playerRef.getUuid()); + + world.execute(() -> { + playerRef.sendMessage(Message.raw("Operation successful!")); + }); + } catch (Exception e) { + getLogger().at(Level.SEVERE).log("Async operation failed: " + e.getMessage()); + + world.execute(() -> { + playerRef.sendMessage(Message.raw("Operation failed. Please try again.")); + }); + } + }); + } + + // ============================================= + // Chaining Async Operations - CORRECT + // ============================================= + public void chainedOperations(PlayerRef playerRef, World world) { + CompletableFuture + .supplyAsync(() -> { + // First async operation + return fetchFromDatabase(playerRef.getUuid()); + }) + .thenApplyAsync(data -> { + // Second async operation + return processData(data); + }) + .thenAccept(result -> { + // Return to world thread with final result + world.execute(() -> { + playerRef.sendMessage(Message.raw("Final result: " + result)); + }); + }) + .exceptionally(e -> { + getLogger().at(Level.SEVERE).log("Chain failed: " + e.getMessage()); + world.execute(() -> { + playerRef.sendMessage(Message.raw("Operation failed.")); + }); + return null; + }); + } + + // Helper methods + private void saveAllData() { } + private String simulateDatabaseLoad(UUID uuid) { return "data"; } + private String simulateHeavyComputation(UUID uuid) { return "result"; } + private void riskyOperation(UUID uuid) throws Exception { } + private String fetchFromDatabase(UUID uuid) { return "data"; } + private String processData(String data) { return "processed"; } +} diff --git a/app/src/main/java/org/example/docs/ui/CustomPagesExample.java b/app/src/main/java/org/example/docs/ui/CustomPagesExample.java new file mode 100644 index 0000000..309c598 --- /dev/null +++ b/app/src/main/java/org/example/docs/ui/CustomPagesExample.java @@ -0,0 +1,208 @@ +package org.example.docs.ui; + +/** + * Example code from: hytale-docs/content/ui/custom-pages.en.md + * Tests the Custom Pages documentation + */ + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.protocol.packets.interface_.Page; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.CustomUIPage; +import com.hypixel.hytale.server.core.entity.entities.player.pages.PageManager; +import com.hypixel.hytale.server.core.entity.entities.player.windows.ContainerWindow; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +import java.util.List; + +public class CustomPagesExample { + + // Example from documentation - WelcomePage + public static class WelcomePage extends CustomUIPage { + + public WelcomePage(PlayerRef playerRef) { + super(playerRef, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + // Load a UI template from assets + builder.append("ui/welcome_screen"); + + // Set dynamic values + builder.set("#player_name", playerRef.getUsername()); + builder.set("#server_name", "My Server"); + } + } + + // Example from documentation - MenuPage with events + public static class MenuPage extends CustomUIPage { + + public MenuPage(PlayerRef ref) { + super(ref, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + builder.append("ui/main_menu"); + + // Use addEventBinding with CustomUIEventBindingType and EventData + events.addEventBinding(CustomUIEventBindingType.Activating, "#play_button", + EventData.of("Action", "play")); + events.addEventBinding(CustomUIEventBindingType.Activating, "#quit_button", + EventData.of("Action", "quit")); + } + + @Override + public void handleDataEvent(Ref ref, Store store, String rawData) { + // rawData contains JSON like: {"Action":"play"} + // Parse and handle accordingly + if (rawData.contains("\"Action\":\"play\"")) { + startGame(); + } else if (rawData.contains("\"Action\":\"quit\"")) { + close(); + } + } + + private void startGame() { + // Game logic... + close(); // Close the page + } + } + + // Example from documentation - Opening Pages + public void openingPagesExample(Player player) { + PageManager pageManager = player.getPageManager(); + + // Open custom page + pageManager.openCustomPage( + player.getReference(), + player.getReference().getStore(), + new MenuPage(player.getPlayerRef()) + ); + } + + // Example from documentation - Closing Pages + public void closingPagesExample(Player player) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + // From outside + pageManager.setPage(ref, store, Page.None); + } + + // Example from documentation - Built-in pages + // NOTE: Documentation was wrong - Page.Settings doesn't exist + // Available: None, Bench, Inventory, ToolsSettings, Map, MachinimaEditor, ContentCreation, Custom + public void builtInPagesExample(Player player) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + // Open built-in page (e.g., Inventory) + pageManager.setPage(ref, store, Page.Inventory); + } + + // Example from documentation - ShopPage + public static class ShopItem { + private final String name; + private final int price; + + public ShopItem(String name, int price) { + this.name = name; + this.price = price; + } + + public String getName() { return name; } + public int getPrice() { return price; } + } + + public static class ShopPage extends CustomUIPage { + + private int selectedItem = -1; + private final List items; + + public ShopPage(PlayerRef ref, List items) { + super(ref, CustomPageLifetime.CanDismiss); + this.items = items; + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + builder.append("ui/shop"); + + // Populate items + for (int i = 0; i < items.size(); i++) { + ShopItem item = items.get(i); + builder.set("#item_" + i + "_name", item.getName()); + builder.set("#item_" + i + "_price", item.getPrice()); + events.addEventBinding(CustomUIEventBindingType.Activating, "#item_" + i, + EventData.of("Action", "select").append("Index", String.valueOf(i))); + } + + events.addEventBinding(CustomUIEventBindingType.Activating, "#buy_button", + EventData.of("Action", "buy")); + events.addEventBinding(CustomUIEventBindingType.Activating, "#close_button", + EventData.of("Action", "close")); + } + + @Override + public void handleDataEvent(Ref ref, Store store, String rawData) { + // rawData contains JSON like: {"Action":"select","Index":"0"} + if (rawData.contains("\"Action\":\"select\"")) { + // Extract index from JSON + int indexStart = rawData.indexOf("\"Index\":\"") + 9; + int indexEnd = rawData.indexOf("\"", indexStart); + selectedItem = Integer.parseInt(rawData.substring(indexStart, indexEnd)); + updateSelection(); + } else if (rawData.contains("\"Action\":\"buy\"") && selectedItem >= 0) { + purchaseItem(selectedItem); + } else if (rawData.contains("\"Action\":\"close\"")) { + close(); + } + } + + private void updateSelection() { + UICommandBuilder update = new UICommandBuilder(); + update.set("#selected_index", selectedItem); + update.set("#selected_name", items.get(selectedItem).getName()); + sendUpdate(update); + } + + private void purchaseItem(int index) { + // Purchase logic... + rebuild(); // Refresh UI + } + } + + // Example from documentation - Page with Windows + // NOTE: The documentation shows openCustomPageWithWindows but the signature is: + // openCustomPageWithWindows(Ref, Store, CustomUIPage, Window...) + public void pageWithWindowsExample(Player player, ItemContainer craftingContainer) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + // Custom crafting UI with windows + // NOTE: ContainerWindow takes just ItemContainer, not windowId/WindowType + ContainerWindow inputWindow = new ContainerWindow(craftingContainer); + + pageManager.openCustomPageWithWindows( + ref, store, + new WelcomePage(player.getPlayerRef()), + inputWindow + ); + } +} diff --git a/app/src/main/java/org/example/docs/ui/HudManagerExample.java b/app/src/main/java/org/example/docs/ui/HudManagerExample.java new file mode 100644 index 0000000..348b985 --- /dev/null +++ b/app/src/main/java/org/example/docs/ui/HudManagerExample.java @@ -0,0 +1,149 @@ +package org.example.docs.ui; + +/** + * Example code from: hytale-docs/content/ui/hud-manager.en.md + * Tests the HUD Manager documentation + */ + +import com.hypixel.hytale.protocol.packets.interface_.HudComponent; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.hud.CustomUIHud; +import com.hypixel.hytale.server.core.entity.entities.player.hud.HudManager; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; + +import java.util.HashSet; +import java.util.Set; + +public class HudManagerExample { + + // Example from documentation - Accessing HUD Manager + public void accessingHudManager(Player player) { + HudManager hudManager = player.getHudManager(); + } + + // Example from documentation - Showing Components + public void showingComponents(Player player) { + HudManager hudManager = player.getHudManager(); + + // Show specific components (additive) + hudManager.showHudComponents(player.getPlayerRef(), + HudComponent.Health, + HudComponent.Stamina, + HudComponent.Hotbar + ); + + // Show from a Set + Set components = Set.of( + HudComponent.Chat, + HudComponent.Notifications + ); + hudManager.showHudComponents(player.getPlayerRef(), components); + } + + // Example from documentation - Hiding Components + public void hidingComponents(Player player) { + HudManager hudManager = player.getHudManager(); + + // Hide specific components + hudManager.hideHudComponents(player.getPlayerRef(), + HudComponent.Compass, + HudComponent.Speedometer + ); + } + + // Example from documentation - Setting Components + public void settingComponents(Player player) { + HudManager hudManager = player.getHudManager(); + + // Set exact visible components (replaces all) + hudManager.setVisibleHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health + ); + + // Clear all HUD components (empty) + hudManager.setVisibleHudComponents(player.getPlayerRef()); + } + + // Example from documentation - Querying Visible Components + public void queryingComponents(Player player) { + HudManager hudManager = player.getHudManager(); + + Set visible = hudManager.getVisibleHudComponents(); + + if (visible.contains(HudComponent.Health)) { + // Health bar is visible + } + } + + // Example from documentation - Resetting HUD + public void resettingHud(Player player) { + HudManager hudManager = player.getHudManager(); + + // Reset to default components and clear custom HUD + hudManager.resetHud(player.getPlayerRef()); + + // Reset entire UI state + hudManager.resetUserInterface(player.getPlayerRef()); + } + + // Example from documentation - Custom HUD Overlay + public static class ScoreboardHud extends CustomUIHud { + + public ScoreboardHud(PlayerRef ref) { + super(ref); + } + + @Override + protected void build(UICommandBuilder builder) { + builder.append("ui/scoreboard"); + builder.set("#score", 1500); + builder.set("#rank", "Gold"); + } + } + + public void customHudOverlay(Player player) { + HudManager hudManager = player.getHudManager(); + + // Set custom HUD + hudManager.setCustomHud(player.getPlayerRef(), new ScoreboardHud(player.getPlayerRef())); + + // Remove custom HUD + hudManager.setCustomHud(player.getPlayerRef(), null); + } + + // Example from documentation - Cinematic Mode + public void enterCinematicMode(Player player) { + HudManager hud = player.getHudManager(); + + // Store current state if needed for restoration + Set previous = new HashSet<>(hud.getVisibleHudComponents()); + + // Clear all HUD + hud.setVisibleHudComponents(player.getPlayerRef()); + } + + public void exitCinematicMode(Player player, Set restore) { + player.getHudManager().setVisibleHudComponents(player.getPlayerRef(), restore); + } + + // Example from documentation - Minimal HUD Mode + public void setMinimalHud(Player player) { + player.getHudManager().setVisibleHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health, + HudComponent.Reticle + ); + } + + // Example from documentation - Builder Mode HUD + public void enableBuilderHud(Player player) { + HudManager hud = player.getHudManager(); + hud.showHudComponents(player.getPlayerRef(), + HudComponent.BuilderToolsLegend, + HudComponent.BuilderToolsMaterialSlotSelector, + HudComponent.BlockVariantSelector + ); + } +} diff --git a/app/src/main/java/org/example/docs/ui/UiIndexExample.java b/app/src/main/java/org/example/docs/ui/UiIndexExample.java new file mode 100644 index 0000000..3ee7fe4 --- /dev/null +++ b/app/src/main/java/org/example/docs/ui/UiIndexExample.java @@ -0,0 +1,66 @@ +package org.example.docs.ui; + +/** + * Example code from: hytale-docs/content/ui/_index.en.md + * Tests the UI system overview documentation + */ + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.HudComponent; +import com.hypixel.hytale.protocol.packets.interface_.Page; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.hud.HudManager; +import com.hypixel.hytale.server.core.entity.entities.player.pages.CustomUIPage; +import com.hypixel.hytale.server.core.entity.entities.player.pages.PageManager; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +public class UiIndexExample { + + // Example from documentation - Custom Page + public static class MyPage extends CustomUIPage { + + public MyPage(PlayerRef ref) { + super(ref, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + // Build UI using builder.set(), builder.append(), etc. + builder.append("ui/my_template"); + builder.set("#title", "Welcome!"); + } + } + + // Example from documentation - Page Manager + public void pageManagerExample(Player player) { + Ref ref = player.getReference(); + Store store = ref.getStore(); + PageManager pageManager = player.getPageManager(); + + // Open a custom page + pageManager.openCustomPage(ref, store, new MyPage(player.getPlayerRef())); + + // Close to normal view + pageManager.setPage(ref, store, Page.None); + } + + // Example from documentation - HUD Manager + public void hudManagerExample(Player player) { + HudManager hudManager = player.getHudManager(); + + // Hide all HUD components + hudManager.setVisibleHudComponents(player.getPlayerRef()); + + // Show specific components + hudManager.showHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health + ); + } +} diff --git a/app/src/main/java/org/example/docs/ui/WindowsExample.java b/app/src/main/java/org/example/docs/ui/WindowsExample.java new file mode 100644 index 0000000..61899d0 --- /dev/null +++ b/app/src/main/java/org/example/docs/ui/WindowsExample.java @@ -0,0 +1,83 @@ +package org.example.docs.ui; + +/** + * Example code from: hytale-docs/content/ui/windows.en.md + * Tests the Windows documentation + * + * NOTE: The documentation has some errors: + * - ContainerWindow constructor is ContainerWindow(ItemContainer), not ContainerWindow(id, WindowType, container) + * - BlockWindow is abstract and cannot be instantiated directly + * - ItemStackContainerWindow constructor is ItemStackContainerWindow(ItemStackItemContainer) + */ + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.Page; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.PageManager; +import com.hypixel.hytale.server.core.entity.entities.player.windows.ContainerWindow; +import com.hypixel.hytale.server.core.entity.entities.player.windows.ItemStackContainerWindow; +import com.hypixel.hytale.server.core.entity.entities.player.windows.WindowManager; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.inventory.container.ItemStackItemContainer; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +public class WindowsExample { + + // Example from documentation - Window Manager + public void windowManagerExample(Player player) { + WindowManager windowManager = player.getWindowManager(); + } + + // Example from documentation - ContainerWindow + // CORRECTED: Constructor is ContainerWindow(ItemContainer) + public void containerWindowExample(ItemContainer container) { + // Open a container window + ContainerWindow window = new ContainerWindow(container); + } + + // Example from documentation - ItemStackContainerWindow + // CORRECTED: Constructor is ItemStackContainerWindow(ItemStackItemContainer) + public void itemStackContainerWindowExample(ItemStackItemContainer itemStackContainer) { + ItemStackContainerWindow window = new ItemStackContainerWindow(itemStackContainer); + } + + // Example from documentation - Opening Windows + // CORRECTED: ContainerWindow takes just ItemContainer + public void openingWindowsExample(Player player, ItemContainer chestContainer) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + // Open page with inventory windows + ContainerWindow chestWindow = new ContainerWindow(chestContainer); + + boolean success = pageManager.setPageWithWindows( + ref, store, + Page.None, // or Page.Custom + true, // canCloseThroughInteraction + chestWindow + ); + } + + // Example from documentation - Chest Container UI + // CORRECTED: ContainerWindow takes just ItemContainer + public void openChest(Player player, ItemContainer chestContainer) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + ContainerWindow window = new ContainerWindow(chestContainer); + + pageManager.setPageWithWindows( + ref, store, + Page.None, + true, // Allow closing via interaction + window + ); + } + + // NOTE: BlockWindow is abstract - cannot be instantiated directly + // The documentation example is incorrect + // You would need to use a concrete subclass or the builtin interactions +} diff --git a/app/src/main/java/org/example/docs/world/BlocksExample.java b/app/src/main/java/org/example/docs/world/BlocksExample.java new file mode 100644 index 0000000..5cd8517 --- /dev/null +++ b/app/src/main/java/org/example/docs/world/BlocksExample.java @@ -0,0 +1,197 @@ +package org.example.docs.world; + +// Example from: hytale-docs/content/world/blocks.en.md +// Tests the blocks documentation examples +// +// FINDINGS: +// - Vector3i uses getX(), getY(), getZ() - NOT x(), y(), z() +// - Rotation values are None, Ninety, OneEighty, TwoSeventy - NOT North, East, etc. +// - World does NOT implement BlockAccessor directly - placeBlock is on WorldChunk +// - For placeBlock with rotation, you MUST get the chunk first: +// WorldChunk chunk = world.getChunk(ChunkUtil.indexChunkFromBlock(x, z)); +// chunk.placeBlock(x, y, z, "block_key", rotation...); +// - World-level methods available: setBlock, getBlock, breakBlock, getBlockType + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +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.universe.world.chunk.WorldChunk; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple; +import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent; +import com.hypixel.hytale.server.core.event.events.ecs.PlaceBlockEvent; +import com.hypixel.hytale.server.core.event.events.ecs.DamageBlockEvent; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.math.vector.Vector3i; +import com.hypixel.hytale.protocol.BlockMaterial; + +import java.util.logging.Level; + +/** + * Tests blocks.en.md documentation + */ +public class BlocksExample extends JavaPlugin { + + public BlocksExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // ============================================= + // Block Events + // ============================================= + + // BreakBlockEvent + getEventRegistry().register(BreakBlockEvent.class, event -> { + Vector3i position = event.getTargetBlock(); + BlockType blockType = event.getBlockType(); + + getLogger().at(Level.INFO).log("Block broken at " + position); + + // Cancel if needed + // event.setCancelled(true); + }); + + // PlaceBlockEvent + getEventRegistry().register(PlaceBlockEvent.class, event -> { + Vector3i position = event.getTargetBlock(); + RotationTuple rotation = event.getRotation(); + + getLogger().at(Level.INFO).log("Block placed at " + position); + + // Cancel if needed + // event.setCancelled(true); + }); + + // DamageBlockEvent + getEventRegistry().register(DamageBlockEvent.class, event -> { + Vector3i position = event.getTargetBlock(); + BlockType blockType = event.getBlockType(); + + // Cancel to prevent damage + // event.setCancelled(true); + }); + } + + // ============================================= + // Block manipulation examples + // ============================================= + public void blockManipulationExamples(World world) { + int x = 100, y = 64, z = 200; + + // Get block ID at position + int blockId = world.getBlock(x, y, z); + int blockIdVec = world.getBlock(new Vector3i(x, y, z)); + + // Get BlockType at position + BlockType blockType = world.getBlockType(x, y, z); + BlockType blockTypeVec = world.getBlockType(new Vector3i(x, y, z)); + + // Check if block is empty + if (blockType == BlockType.EMPTY) { + // Air or empty space + } + + // Set block by block type key + world.setBlock(x, y, z, "stone"); + world.setBlock(x, y, z, "oak_planks", 0); + + // Set block by BlockType object - use getId() to get the key string + BlockType stone = BlockType.getAssetMap().getAsset("stone"); + if (stone != null) { + world.setBlock(x, y, z, stone.getId()); + } + + // Get block ID index (useful for internal operations) + int stoneId = BlockType.getAssetMap().getIndex("stone"); + // NOTE: World.setBlock takes a String blockTypeKey, NOT a block ID int + // Use the string key directly: + world.setBlock(x, y, z, "stone"); + + // Place block with rotation + // NOTE: placeBlock is on WorldChunk (BlockAccessor), NOT on World directly! + // Must get the chunk first + WorldChunk chunk = world.getChunk(ChunkUtil.indexChunkFromBlock(x, z)); + if (chunk != null) { + // Rotation values are None, Ninety, OneEighty, TwoSeventy + chunk.placeBlock(x, y, z, "oak_log", + Rotation.Ninety, // yaw (90 degrees) + Rotation.None, // pitch + Rotation.None // roll + ); + + // Place with rotation tuple + chunk.placeBlock(x, y, z, "oak_log", + RotationTuple.of(Rotation.Ninety, Rotation.None, Rotation.None), + 0, // settings + true // validatePlacement + ); + } + + // Break block (requires settings parameter) + world.breakBlock(x, y, z, 0); // 0 = default settings + + // Block interaction state + BlockType currentBlock = world.getBlockType(x, y, z); + if (currentBlock != null) { + world.setBlockInteractionState( + new Vector3i(x, y, z), + currentBlock, + "open" + ); + } + } + + // ============================================= + // BlockType asset access + // ============================================= + public void blockTypeExamples() { + // Get block type by ID + BlockType blockType = BlockType.getAssetMap().getAsset("stone"); + + if (blockType != null) { + // Get block ID + String blockId = blockType.getId(); + + // Get material + BlockMaterial material = blockType.getMaterial(); + } + + // Get block ID index + int blockIndex = BlockType.getAssetMap().getIndex("stone"); + + // Get block from index + BlockType fromIndex = BlockType.getAssetMap().getAsset(blockIndex); + } + + // ============================================= + // Utility methods + // NOTE: Uses getX(), getY(), getZ() - NOT x(), y(), z() + // ============================================= + public boolean containsBlockType(World world, Vector3i min, Vector3i max, String blockTypeKey) { + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + BlockType block = world.getBlockType(x, y, z); + if (block != null && block.getId().equals(blockTypeKey)) { + return true; + } + } + } + } + return false; + } + + public void fillArea(World world, Vector3i min, Vector3i max, String blockTypeKey) { + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + world.setBlock(x, y, z, blockTypeKey); + } + } + } + } +} diff --git a/app/src/main/java/org/example/docs/world/ChunksExample.java b/app/src/main/java/org/example/docs/world/ChunksExample.java new file mode 100644 index 0000000..3e8dfbc --- /dev/null +++ b/app/src/main/java/org/example/docs/world/ChunksExample.java @@ -0,0 +1,156 @@ +package org.example.docs.world; + +// Example from: hytale-docs/content/world/chunks.en.md +// Tests the chunks documentation examples +// +// FINDINGS: +// - Vector3d uses public fields x, y, z OR getX(), getY(), getZ() - NOT x(), y(), z() +// - Vector3i uses getX(), getY(), getZ() - NOT x(), y(), z() +// - Player.getPosition() does NOT exist - use PlayerRef.getTransform().getPosition() +// - Vector2i uses getX(), getY() - NOT x(), y() + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +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.universe.PlayerRef; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3i; +import com.hypixel.hytale.math.vector.Vector2i; +import com.hypixel.hytale.math.vector.Transform; + +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +/** + * Tests chunks.en.md documentation + * + * IMPORTANT: + * - Player.getPosition() DOESN'T EXIST - use PlayerRef.getTransform().getPosition() + * - Vector3d uses x, y, z fields or getX(), getY(), getZ() - NOT x(), y(), z() + * - Vector3i uses getX(), getY(), getZ() - NOT x(), y(), z() + */ +public class ChunksExample extends JavaPlugin { + + public ChunksExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // ============================================= + // Chunk Basics + // ============================================= + World world = Universe.get().getWorld("default"); + int x = 100, y = 64, z = 200; + + // Access blocks within a chunk through the World API + BlockType blockType = world.getBlockType(x, y, z); + + // ============================================= + // Chunk Coordinates + // ============================================= + int blockX = 100, blockZ = 200; + + // Convert block coordinates to chunk coordinates + int chunkX = blockX >> 4; // Divide by 16 + int chunkZ = blockZ >> 4; + + // Convert chunk coordinates to block coordinates (corner) + int startBlockX = chunkX << 4; // Multiply by 16 + int startBlockZ = chunkZ << 4; + + // ============================================= + // Chunk Loading - CORRECTED + // ============================================= + getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + + // CORRECT: Get position from PlayerRef.getTransform() + // Player.getPosition() does NOT exist! + Transform transform = playerRef.getTransform(); + if (transform != null) { + Vector3d pos = transform.getPosition(); + // Vector3d uses getX(), getY(), getZ() - NOT x(), y(), z() + int pChunkX = (int) pos.getX() >> 4; + int pChunkZ = (int) pos.getZ() >> 4; + + getLogger().at(Level.INFO).log("Player at chunk: " + pChunkX + ", " + pChunkZ); + } + }); + } + + // ============================================= + // Working with Chunks + // ============================================= + public void iterateChunkBlocks(World world, int chunkX, int chunkZ) { + int startX = chunkX << 4; + int startZ = chunkZ << 4; + + for (int x = startX; x < startX + 16; x++) { + for (int z = startZ; z < startZ + 16; z++) { + for (int y = 0; y < 256; y++) { + BlockType blockType = world.getBlockType(x, y, z); + // Process block... + } + } + } + } + + public void processChunk(World world, int chunkX, int chunkZ) { + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int worldX = baseX + x; + int worldZ = baseZ + z; + // Process... + } + } + } + + // ============================================= + // Async chunk processing - CORRECTED + // ============================================= + public void processChunksAsync(PlayerRef playerRef, World world) { + CompletableFuture.runAsync(() -> { + // Heavy chunk processing here + Map results = new HashMap<>(); + // ... process + + // Return to world thread to apply results + world.execute(() -> { + for (var entry : results.entrySet()) { + Vector3i pos = entry.getKey(); + // CORRECT: Vector3i uses getX(), getY(), getZ() + world.setBlock(pos.getX(), pos.getY(), pos.getZ(), entry.getValue()); + } + playerRef.sendMessage(Message.raw("Chunk processing complete!")); + }); + }); + } + + // ============================================= + // Get chunk coords from player - CORRECTED + // ============================================= + public Vector2i getChunkCoords(PlayerRef playerRef) { + // CORRECT: Use PlayerRef.getTransform() - Player.getPosition() doesn't exist! + Transform transform = playerRef.getTransform(); + if (transform == null) { + return new Vector2i(0, 0); + } + + Vector3d pos = transform.getPosition(); + // CORRECT: Vector3d uses getX(), getZ() - NOT x(), z() + int chunkX = (int) pos.getX() >> 4; + int chunkZ = (int) pos.getZ() >> 4; + return new Vector2i(chunkX, chunkZ); + } +} diff --git a/app/src/main/java/org/example/docs/world/PlayerRefsExample.java b/app/src/main/java/org/example/docs/world/PlayerRefsExample.java new file mode 100644 index 0000000..8e7c41b --- /dev/null +++ b/app/src/main/java/org/example/docs/world/PlayerRefsExample.java @@ -0,0 +1,117 @@ +package org.example.docs.world; + +// Example from: hytale-docs/content/world/player-refs.en.md +// Tests the PlayerRef documentation examples +// +// FINDINGS: +// - PlayerRef.getName() does NOT exist - use getUsername() +// - PlayerRef.getPlayer() does NOT exist +// - PlayerRef.sendMessage(Message) exists directly, not via getPlayer() +// - Player.getPlayerRef() EXISTS + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +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.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.component.Ref; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Tests player-refs.en.md documentation + */ +public class PlayerRefsExample extends JavaPlugin { + private final Map playerData = new ConcurrentHashMap<>(); + + public PlayerRefsExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // ============================================= + // Getting a PlayerRef - CORRECT + // ============================================= + getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef ref = event.getPlayerRef(); + + // PlayerRef methods that EXIST: + UUID uuid = ref.getUuid(); + String name = ref.getUsername(); // NOT getName()! + + // sendMessage is DIRECT on PlayerRef + ref.sendMessage(Message.raw("Welcome!")); + + // Get entity reference + Ref entityRef = ref.getReference(); + + // Check validity + boolean valid = ref.isValid(); + + // Store player data + playerData.put(ref.getUuid(), new PlayerData(ref)); + }); + + // Disconnect handling + getEventRegistry().register(PlayerDisconnectEvent.class, event -> { + playerData.remove(event.getPlayerRef().getUuid()); + }); + } + + // ============================================= + // World PlayerRefs - CORRECT + // ============================================= + public void worldPlayerRefs() { + World world = Universe.get().getWorld("default"); + if (world != null) { + // Get all player refs in this world + Collection playerRefs = world.getPlayerRefs(); + + // Get all active players (returns List, not Collection) + List players = world.getPlayers(); + } + } + + // ============================================= + // Cross-World Messaging - CORRECT + // ============================================= + public void sendGlobalMessage(PlayerRef ref, String message) { + // PlayerRef.sendMessage handles cross-world messaging directly + ref.sendMessage(Message.raw(message)); + } + + public void broadcastAll(String message) { + Message msg = Message.raw(message); + for (World world : Universe.get().getWorlds().values()) { + for (PlayerRef ref : world.getPlayerRefs()) { + ref.sendMessage(msg); + } + } + } + + // Simple player data class + static class PlayerData { + private final PlayerRef playerRef; + private int score; + + public PlayerData(PlayerRef ref) { + this.playerRef = ref; + } + + public void addScore(int amount) { + this.score += amount; + playerRef.sendMessage(Message.raw("Score: " + score)); + } + } +} diff --git a/app/src/main/java/org/example/docs/world/UniverseWorldsExample.java b/app/src/main/java/org/example/docs/world/UniverseWorldsExample.java new file mode 100644 index 0000000..78badb0 --- /dev/null +++ b/app/src/main/java/org/example/docs/world/UniverseWorldsExample.java @@ -0,0 +1,106 @@ +package org.example.docs.world; + +// Example from: hytale-docs/content/world/universe-and-worlds.en.md +// Tests the universe and worlds documentation examples +// +// FINDINGS: +// - universe.hasWorld() does NOT exist - use universe.getWorld(name) != null +// - world.getEntities() does NOT exist on World +// - world.setBlock(x, y, z, blockType) wrong - takes String, not BlockType +// - world.getSpawnPoint() does NOT exist - access via WorldConfig.getSpawnProvider() +// - world.getPlayers() returns List, not Collection + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +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.universe.PlayerRef; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.math.vector.Vector3i; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +/** + * Tests universe-and-worlds.en.md documentation + */ +public class UniverseWorldsExample extends JavaPlugin { + + public UniverseWorldsExample(JavaPluginInit init) { + super(init); + } + + @Override + public void setup() { + // ============================================= + // Universe - CORRECT patterns + // ============================================= + Universe universe = Universe.get(); + + // Get specific world + World world = universe.getWorld("default"); + + // Get all worlds + Map worlds = universe.getWorlds(); + + // Check if world exists - CORRECT way (hasWorld doesn't exist!) + if (universe.getWorld("creative") != null) { + // World exists + } + + // ============================================= + // World - CORRECT patterns + // ============================================= + if (world != null) { + // Get world name + String name = world.getName(); + + // Get all players - returns List, not Collection + List players = world.getPlayers(); + + // Get player refs + Collection playerRefs = world.getPlayerRefs(); + + // NOTE: world.getEntities() does NOT exist on World! + // Entities are accessed through EntityStore + } + + // ============================================= + // World Operations - CORRECT patterns + // ============================================= + if (world != null) { + int x = 100, y = 64, z = 200; + + // Get block at position + BlockType blockType = world.getBlockType(x, y, z); + BlockType blockTypeVec = world.getBlockType(new Vector3i(x, y, z)); + + // Set block - takes String, NOT BlockType! + world.setBlock(x, y, z, "stone"); + + // If you have a BlockType, use getId() + if (blockType != null) { + world.setBlock(x, y, z, blockType.getId()); + } + + // NOTE: world.getSpawnPoint() does NOT exist directly! + // Access spawn via: world.getWorldConfig().getSpawnProvider() + } + + // ============================================= + // World from Event - CORRECT + // ============================================= + getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + World eventWorld = event.getWorld(); // Can be null! + + if (eventWorld != null) { + getLogger().at(Level.INFO).log("Player joined world: " + eventWorld.getName()); + } + }); + } +} diff --git a/app/src/main/resources/manifest.json b/app/src/main/resources/manifest.json new file mode 100644 index 0000000..9a786e9 --- /dev/null +++ b/app/src/main/resources/manifest.json @@ -0,0 +1,18 @@ +{ + "Group": "org.example", + "Name": "ExamplePlugin", + "Version": "1.0.0", + "Description": "An example Hytale server plugin", + "Authors": [ + { + "Name": "Your Name" + } + ], + "Main": "org.example.ExamplePlugin", + "ServerVersion": "*", + "Dependencies": {}, + "OptionalDependencies": {}, + "DisabledByDefault": false, + "IncludesAssetPack": false, + "SubPlugins": [] +} diff --git a/app/src/test/java/org/example/ExamplePluginTest.java b/app/src/test/java/org/example/ExamplePluginTest.java new file mode 100644 index 0000000..66f2679 --- /dev/null +++ b/app/src/test/java/org/example/ExamplePluginTest.java @@ -0,0 +1,12 @@ +package org.example; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class ExamplePluginTest { + @Test + public void pluginJsonExists() { + // Verify manifest.json exists in resources + assertNotNull(getClass().getClassLoader().getResource("manifest.json")); + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..a600976 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + java +} + +// Root project configuration +// The actual plugin code is in the 'app' module diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..377538c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..f50a589 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,10 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +guava = "33.4.6-jre" +junit = "4.13.2" + +[libraries] +guava = { module = "com.google.guava:guava", version.ref = "guava" } +junit = { module = "junit:junit", version.ref = "junit" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f8e1ee3125fe0768e9a76ee977ac089eb657005e GIT binary patch literal 45633 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�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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..23449a2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..08530dc --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,7 @@ +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +rootProject.name = "example-plugin" +include("app") diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..5e17c1d --- /dev/null +++ b/setup.sh @@ -0,0 +1,140 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BIN_DIR="$SCRIPT_DIR/.bin" +SERVER_DIR="$SCRIPT_DIR/server" +SRC_REF_DIR="$SCRIPT_DIR/src-ref" + + +# Détection de l'OS +detect_os() { + case "$(uname -s)" in + Linux*) echo "linux-amd64" ;; + Darwin*) echo "darwin-amd64" ;; + CYGWIN*|MINGW*|MSYS*) echo "windows-amd64.exe" ;; + *) echo "unknown" ;; + esac +} + +OS_SUFFIX=$(detect_os) +if [ "$OS_SUFFIX" = "unknown" ]; then + echo "Erreur: OS non supporté" + exit 1 +fi + +download() { + mkdir -p "$BIN_DIR" + + echo "Téléchargement de hytale-downloader..." + curl -L -o "$BIN_DIR/hytale-downloader.zip" "https://downloader.hytale.com/hytale-downloader.zip" + unzip -o "$BIN_DIR/hytale-downloader.zip" -d "$BIN_DIR" + rm "$BIN_DIR/hytale-downloader.zip" + + echo "Téléchargement de CFR..." + curl -L -o "$BIN_DIR/cfr-0.152.jar" "https://www.benf.org/other/cfr/cfr-0.152.jar" + + # Rendre les binaires exécutables + chmod +x "$BIN_DIR"/hytale-downloader-* 2>/dev/null || true + + echo "Téléchargement terminé." +} + +setup() { + + DOWNLOADER="$BIN_DIR/hytale-downloader-$OS_SUFFIX" + if [ ! -f "$DOWNLOADER" ]; then + echo "Erreur: $DOWNLOADER non trouvé." + echo "Exécutez d'abord: ./setup.sh --download" + exit 1 + fi + + echo "Téléchargement du serveur Hytale..." + if [ ! -d "$SERVER_DIR" ]; then + mkdir $SERVER_DIR + fi + if [ ! -f "$SERVER_DIR/server.zip" ]; then + "$DOWNLOADER" --download-path "$SERVER_DIR/server.zip" + fi + echo "Décompression de l'archive du serveur Hytale" + unzip -o "$SERVER_DIR/server.zip" -d "$SERVER_DIR" + echo "Téléchargement terminée." +} + +update_safe() { + echo "Are you sure ?" + echo "It will delete the old server/Assets.zip server/server.zip server/Server/* " + echo "Use ./setup.sh --update-sure" +} + +update() { + DOWNLOADER="$BIN_DIR/hytale-downloader-$OS_SUFFIX" + echo "Téléchargement du serveur Hytale..." + if [ ! -d "$SERVER_DIR" ]; then + mkdir $SERVER_DIR + fi + if [ -f "$SERVER_DIR/server.zip" ]; then + rm $SERVER_DIR/server.zip + fi + if [ -f "$SERVER_DIR/Assets.zip" ]; then + rm $SERVER_DIR/Assets.zip + fi + if [ -d "$SERVER_DIR/Server.zip" ]; then + rm -rf $SERVER_DIR/Server + fi + if [ ! -f "$SERVER_DIR/server.zip" ]; then + "$DOWNLOADER" --download-path "$SERVER_DIR/server.zip" + fi + echo "Décompression de l'archive du serveur Hytale" + unzip -o "$SERVER_DIR/server.zip" -d "$SERVER_DIR" + echo "Téléchargement terminée." +} + +update-pre() { + DOWNLOADER="$BIN_DIR/hytale-downloader-$OS_SUFFIX" + echo "Téléchargement du serveur Hytale..." + if [ ! -d "$SERVER_DIR" ]; then + mkdir $SERVER_DIR + fi + if [ -f "$SERVER_DIR/server.zip" ]; then + rm $SERVER_DIR/server.zip + fi + if [ -f "$SERVER_DIR/Assets.zip" ]; then + rm $SERVER_DIR/Assets.zip + fi + if [ -d "$SERVER_DIR/Server.zip" ]; then + rm -rf $SERVER_DIR/Server + fi + if [ ! -f "$SERVER_DIR/server.zip" ]; then + "$DOWNLOADER" --download-path "$SERVER_DIR/server.zip" --patchline pre-release + fi + echo "Décompression de l'archive du serveur Hytale" + unzip -o "$SERVER_DIR/server.zip" -d "$SERVER_DIR" + echo "Téléchargement terminée." +} + +decompile() { + echo "Décompilation avec CFR..." + rm -rf "$SRC_REF_DIR/*" + java -jar "$BIN_DIR/cfr-0.152.jar" "$SERVER_DIR/Server/HytaleServer.jar" --outputdir "$SRC_REF_DIR" + echo "Décompilation terminée." +} + +case "$1" in + --download) download ;; + --setup) setup ;; + --decompile)decompile ;; + --update) update ;; + --update-pre) update-pre ;; + *) + echo "Usage: ./setup.sh [--download|--setup]" + echo "" + echo "Options:" + echo " --download Télécharge les outils (hytale-downloader, CFR)" + echo " --setup Configure l'environnement (télécharge le serveur)" + echo " --update Télécharge la mise à jour" + echo " --update-pre Télécharge la mise à jour pre-release" + echo " --decompile Décompile le serveur" + exit 1 + ;; +esac