diff --git a/core/src/Command.cpp b/core/src/Command.cpp new file mode 100644 index 0000000..767895b --- /dev/null +++ b/core/src/Command.cpp @@ -0,0 +1,777 @@ +#include "vf/Command.h" + +#include +#include +#include +#include +#include + +#include "vf/Structures.h" + +namespace vf { + +namespace { + +struct NamedBlock { + const char* name; + Block block; +}; + +struct NamedItem { + const char* name; + ItemId id; +}; + +struct NamedSpecies { + const char* name; + Species species; +}; + +struct NamedStructure { + const char* name; + StructureGenKind kind; +}; + +const NamedBlock blockTable[] = { + {"air", Block::Air}, {"grass", Block::Grass}, {"dirt", Block::Dirt}, + {"stone", Block::Stone}, {"sand", Block::Sand}, {"wood", Block::Wood}, + {"leaves", Block::Leaves}, {"water", Block::Water}, {"snow", Block::Snow}, + {"coalOre", Block::CoalOre}, {"ironOre", Block::IronOre}, {"goldOre", Block::GoldOre}, + {"gravel", Block::Gravel}, {"glowstone", Block::Glowstone}, + {"cobblestone", Block::Cobblestone}, {"planks", Block::Planks}, + {"sandstone", Block::Sandstone}, {"ice", Block::Ice}, {"bedrock", Block::Bedrock}, + {"obsidian", Block::Obsidian}, {"lava", Block::Lava}, {"diamondOre", Block::DiamondOre}, + {"redstoneOre", Block::RedstoneOre}, {"emeraldOre", Block::EmeraldOre}, + {"lapisOre", Block::LapisOre}, {"craftingTable", Block::CraftingTable}, + {"furnace", Block::Furnace}, {"furnaceLit", Block::FurnaceLit}, {"chest", Block::Chest}, + {"torch", Block::Torch}, {"redstoneWire", Block::RedstoneWire}, + {"redstoneTorch", Block::RedstoneTorch}, {"redstoneLamp", Block::RedstoneLamp}, + {"redstoneLampLit", Block::RedstoneLampLit}, {"lever", Block::Lever}, + {"button", Block::Button}, {"redstoneBlock", Block::RedstoneBlock}, + {"repeater", Block::Repeater}, {"comparator", Block::Comparator}, + {"piston", Block::Piston}, {"pistonSticky", Block::PistonSticky}, + {"pistonHead", Block::PistonHead}, {"tnt", Block::Tnt}, {"ladder", Block::Ladder}, + {"rail", Block::Rail}, {"poweredRail", Block::PoweredRail}, + {"detectorRail", Block::DetectorRail}, {"activatorRail", Block::ActivatorRail}, + {"hopper", Block::Hopper}, {"dropper", Block::Dropper}, {"dispenser", Block::Dispenser}, + {"observer", Block::Observer}, {"targetBlock", Block::TargetBlock}, + {"slimeBlock", Block::SlimeBlock}, {"noteBlock", Block::NoteBlock}, + {"farmland", Block::Farmland}, {"wheat", Block::Wheat}, {"carrot", Block::Carrot}, + {"potato", Block::Potato}, {"pumpkin", Block::Pumpkin}, {"melon", Block::Melon}, + {"pumpkinStem", Block::PumpkinStem}, {"melonStem", Block::MelonStem}, + {"sugarCane", Block::SugarCane}, {"cactus", Block::Cactus}, {"sapling", Block::Sapling}, + {"vine", Block::Vine}, {"snowLayer", Block::SnowLayer}, {"packedIce", Block::PackedIce}, +}; + +const NamedItem itemTable[] = { + {"grass", ItemId::Grass}, {"dirt", ItemId::Dirt}, {"stone", ItemId::Stone}, + {"sand", ItemId::Sand}, {"wood", ItemId::Wood}, {"leaves", ItemId::Leaves}, + {"snow", ItemId::Snow}, {"coalOre", ItemId::CoalOre}, {"ironOre", ItemId::IronOre}, + {"goldOre", ItemId::GoldOre}, {"gravel", ItemId::Gravel}, {"glowstone", ItemId::Glowstone}, + {"cobblestone", ItemId::Cobblestone}, {"planks", ItemId::Planks}, + {"sandstone", ItemId::Sandstone}, {"ice", ItemId::Ice}, {"obsidian", ItemId::Obsidian}, + {"diamondOre", ItemId::DiamondOre}, {"redstoneOre", ItemId::RedstoneOre}, + {"emeraldOre", ItemId::EmeraldOre}, {"lapisOre", ItemId::LapisOre}, + {"craftingTable", ItemId::CraftingTable}, {"furnace", ItemId::Furnace}, + {"chest", ItemId::Chest}, {"torch", ItemId::Torch}, {"redstoneWire", ItemId::RedstoneWire}, + {"redstoneTorch", ItemId::RedstoneTorch}, {"redstoneLamp", ItemId::RedstoneLamp}, + {"lever", ItemId::Lever}, {"button", ItemId::Button}, + {"redstoneBlock", ItemId::RedstoneBlock}, {"repeater", ItemId::Repeater}, + {"comparator", ItemId::Comparator}, {"piston", ItemId::Piston}, + {"pistonSticky", ItemId::PistonSticky}, {"tnt", ItemId::Tnt}, {"ladder", ItemId::Ladder}, + {"stick", ItemId::Stick}, {"gunpowder", ItemId::Gunpowder}, {"coalLump", ItemId::CoalLump}, + {"ironIngot", ItemId::IronIngot}, {"goldIngot", ItemId::GoldIngot}, + {"diamondGem", ItemId::DiamondGem}, {"emeraldGem", ItemId::EmeraldGem}, + {"redstoneDust", ItemId::RedstoneDust}, {"lapisLazuli", ItemId::LapisLazuli}, + {"woodPickaxe", ItemId::WoodPickaxe}, {"woodAxe", ItemId::WoodAxe}, + {"woodShovel", ItemId::WoodShovel}, {"woodSword", ItemId::WoodSword}, + {"woodHoe", ItemId::WoodHoe}, {"stonePickaxe", ItemId::StonePickaxe}, + {"stoneAxe", ItemId::StoneAxe}, {"stoneShovel", ItemId::StoneShovel}, + {"stoneSword", ItemId::StoneSword}, {"stoneHoe", ItemId::StoneHoe}, + {"ironPickaxe", ItemId::IronPickaxe}, {"ironAxe", ItemId::IronAxe}, + {"ironShovel", ItemId::IronShovel}, {"ironSword", ItemId::IronSword}, + {"ironHoe", ItemId::IronHoe}, {"goldPickaxe", ItemId::GoldPickaxe}, + {"goldAxe", ItemId::GoldAxe}, {"goldShovel", ItemId::GoldShovel}, + {"goldSword", ItemId::GoldSword}, {"goldHoe", ItemId::GoldHoe}, + {"diamondPickaxe", ItemId::DiamondPickaxe}, {"diamondAxe", ItemId::DiamondAxe}, + {"diamondShovel", ItemId::DiamondShovel}, {"diamondSword", ItemId::DiamondSword}, + {"diamondHoe", ItemId::DiamondHoe}, {"apple", ItemId::Apple}, {"bread", ItemId::Bread}, + {"rawPorkchop", ItemId::RawPorkchop}, {"cookedPorkchop", ItemId::CookedPorkchop}, + {"rawBeef", ItemId::RawBeef}, {"cookedBeef", ItemId::CookedBeef}, + {"rawMutton", ItemId::RawMutton}, {"cookedMutton", ItemId::CookedMutton}, + {"rottenFlesh", ItemId::RottenFlesh}, +}; + +const NamedSpecies speciesTable[] = { + {"sheep", Species::Sheep}, {"cow", Species::Cow}, {"pig", Species::Pig}, + {"zombie", Species::Zombie}, {"skeleton", Species::Skeleton}, {"creeper", Species::Creeper}, +}; + +const NamedStructure structureTable[] = { + {"village", StructureGenKind::Village}, {"dungeon", StructureGenKind::Dungeon}, + {"mineshaft", StructureGenKind::Mineshaft}, {"ravine", StructureGenKind::Ravine}, + {"desertTemple", StructureGenKind::DesertTemple}, +}; + +std::string toLower(const std::string& in) { + std::string out(in.size(), '\0'); + for (std::size_t i = 0; i < in.size(); ++i) + out[i] = static_cast(std::tolower(static_cast(in[i]))); + return out; +} + +bool namesMatch(const char* canonical, const std::string& loweredToken) { + std::string lc = toLower(canonical); + return lc == loweredToken; +} + +int blockFloor(float value) { return static_cast(std::floor(value)); } + +long long absll(long long value) { return value < 0 ? -value : value; } + +CommandResult fail(const std::string& message) { + CommandResult out; + out.ok = false; + out.message = message; + return out; +} + +CommandResult okResult(const std::string& message, int affected) { + CommandResult out; + out.ok = true; + out.message = message; + out.affected = affected; + return out; +} + +std::string posText(const Vec3i& p) { + return std::to_string(p.x) + " " + std::to_string(p.y) + " " + std::to_string(p.z); +} + +Vec3i callerBlock(const CommandContext& ctx) { + return {blockFloor(ctx.callerPos.x), blockFloor(ctx.callerPos.y), blockFloor(ctx.callerPos.z)}; +} + +bool inWorldBounds(const CommandContext& ctx, const Vec3i& p) { + if (p.y < ctx.worldMinY || p.y >= ctx.worldMaxY) return false; + if (absll(p.x) > ctx.horizontalLimit || absll(p.z) > ctx.horizontalLimit) return false; + return true; +} + +bool parseCorner(const std::vector& args, std::size_t start, const Vec3i& base, + Vec3i& out) { + return parseCoordArg(args[start], base.x, out.x) && + parseCoordArg(args[start + 1], base.y, out.y) && + parseCoordArg(args[start + 2], base.z, out.z); +} + +void boxExtents(const Vec3i& a, const Vec3i& b, Vec3i& lo, Vec3i& hi) { + lo = {std::min(a.x, b.x), std::min(a.y, b.y), std::min(a.z, b.z)}; + hi = {std::max(a.x, b.x), std::max(a.y, b.y), std::max(a.z, b.z)}; +} + +long long boxVolume(const Vec3i& lo, const Vec3i& hi) { + long long dx = static_cast(hi.x - lo.x) + 1; + long long dy = static_cast(hi.y - lo.y) + 1; + long long dz = static_cast(hi.z - lo.z) + 1; + return dx * dy * dz; +} + +enum class Selector : uint8_t { Self, Nearest, AllEntities, AllPlayers }; + +bool parseSelector(const std::string& token, Selector& out) { + if (token == "@s") { out = Selector::Self; return true; } + if (token == "@p") { out = Selector::Nearest; return true; } + if (token == "@e") { out = Selector::AllEntities; return true; } + if (token == "@a") { out = Selector::AllPlayers; return true; } + return false; +} + +int nearestCreature(const CommandContext& ctx) { + int best = -1; + float bestDistSq = 0.0f; + for (std::size_t i = 0; i < ctx.creatures.size(); ++i) { + const Creature& creature = ctx.creatures[i]; + if (!creature.alive) continue; + Vec3 delta = creature.body.position - ctx.callerPos; + float distSq = dot(delta, delta); + if (best < 0 || distSq < bestDistSq) { + best = static_cast(i); + bestDistSq = distSq; + } + } + return best; +} + +CommandResult cmdSetblock(CommandContext& ctx, const std::vector& args) { + if (args.size() != 4u) return fail("usage: setblock "); + Vec3i target; + if (!parseCorner(args, 0, callerBlock(ctx), target)) + return fail("invalid coordinates"); + Block block; + if (!parseBlockName(args[3], block)) return fail("unknown block: " + args[3]); + if (!inWorldBounds(ctx, target)) return fail("position out of bounds"); + ctx.world.setBlock(target.x, target.y, target.z, block); + CommandResult out = okResult(std::string("placed ") + blockDisplayName(block) + " at " + + posText(target), + 1); + out.hasPosition = true; + out.position = target; + return out; +} + +CommandResult cmdFill(CommandContext& ctx, const std::vector& args) { + if (args.size() != 7u && args.size() != 8u) + return fail("usage: fill [replace|keep|destroy]"); + Vec3i base = callerBlock(ctx); + Vec3i cornerA, cornerB; + if (!parseCorner(args, 0, base, cornerA) || !parseCorner(args, 3, base, cornerB)) + return fail("invalid coordinates"); + Block block; + if (!parseBlockName(args[6], block)) return fail("unknown block: " + args[6]); + + std::string mode = args.size() == 8u ? toLower(args[7]) : "replace"; + if (mode != "replace" && mode != "keep" && mode != "destroy") + return fail("unknown fill mode: " + args[7]); + + Vec3i lo, hi; + boxExtents(cornerA, cornerB, lo, hi); + if (!inWorldBounds(ctx, lo) || !inWorldBounds(ctx, hi)) + return fail("region out of bounds"); + if (boxVolume(lo, hi) > ctx.maxVolume) + return fail("region exceeds max volume " + std::to_string(ctx.maxVolume)); + + int changed = 0; + for (int y = lo.y; y <= hi.y; ++y) + for (int z = lo.z; z <= hi.z; ++z) + for (int x = lo.x; x <= hi.x; ++x) { + if (mode == "keep" && ctx.world.blockAt(x, y, z) != Block::Air) continue; + ctx.world.setBlock(x, y, z, block); + ++changed; + } + return okResult("filled " + std::to_string(changed) + " blocks with " + blockDisplayName(block), + changed); +} + +CommandResult cmdReplace(CommandContext& ctx, const std::vector& args) { + if (args.size() != 8u) + return fail("usage: replace "); + Vec3i base = callerBlock(ctx); + Vec3i cornerA, cornerB; + if (!parseCorner(args, 0, base, cornerA) || !parseCorner(args, 3, base, cornerB)) + return fail("invalid coordinates"); + Block fromBlock, toBlock; + if (!parseBlockName(args[6], fromBlock)) return fail("unknown block: " + args[6]); + if (!parseBlockName(args[7], toBlock)) return fail("unknown block: " + args[7]); + + Vec3i lo, hi; + boxExtents(cornerA, cornerB, lo, hi); + if (!inWorldBounds(ctx, lo) || !inWorldBounds(ctx, hi)) + return fail("region out of bounds"); + if (boxVolume(lo, hi) > ctx.maxVolume) + return fail("region exceeds max volume " + std::to_string(ctx.maxVolume)); + + int changed = 0; + for (int y = lo.y; y <= hi.y; ++y) + for (int z = lo.z; z <= hi.z; ++z) + for (int x = lo.x; x <= hi.x; ++x) { + if (ctx.world.blockAt(x, y, z) != fromBlock) continue; + ctx.world.setBlock(x, y, z, toBlock); + ++changed; + } + return okResult("replaced " + std::to_string(changed) + " blocks", changed); +} + +CommandResult cmdClone(CommandContext& ctx, const std::vector& args) { + if (args.size() != 9u) + return fail("usage: clone "); + Vec3i base = callerBlock(ctx); + Vec3i cornerA, cornerB, dest; + if (!parseCorner(args, 0, base, cornerA) || !parseCorner(args, 3, base, cornerB) || + !parseCorner(args, 6, base, dest)) + return fail("invalid coordinates"); + + Vec3i lo, hi; + boxExtents(cornerA, cornerB, lo, hi); + if (!inWorldBounds(ctx, lo) || !inWorldBounds(ctx, hi)) + return fail("source region out of bounds"); + if (boxVolume(lo, hi) > ctx.maxVolume) + return fail("region exceeds max volume " + std::to_string(ctx.maxVolume)); + + Vec3i extent{hi.x - lo.x, hi.y - lo.y, hi.z - lo.z}; + Vec3i destHi{dest.x + extent.x, dest.y + extent.y, dest.z + extent.z}; + if (!inWorldBounds(ctx, dest) || !inWorldBounds(ctx, destHi)) + return fail("destination region out of bounds"); + + std::vector buffer; + buffer.reserve(static_cast(boxVolume(lo, hi))); + for (int y = lo.y; y <= hi.y; ++y) + for (int z = lo.z; z <= hi.z; ++z) + for (int x = lo.x; x <= hi.x; ++x) + buffer.push_back(ctx.world.blockAt(x, y, z)); + + std::size_t index = 0; + int copied = 0; + for (int y = 0; y <= extent.y; ++y) + for (int z = 0; z <= extent.z; ++z) + for (int x = 0; x <= extent.x; ++x) { + ctx.world.setBlock(dest.x + x, dest.y + y, dest.z + z, buffer[index]); + ++index; + ++copied; + } + return okResult("cloned " + std::to_string(copied) + " blocks", copied); +} + +CommandResult cmdGive(CommandContext& ctx, const std::vector& args) { + if (args.size() != 1u && args.size() != 2u) return fail("usage: give [count]"); + ItemId item; + if (!parseItemName(args[0], item)) return fail("unknown item: " + args[0]); + int count = 1; + if (args.size() == 2u) { + if (!parseIntArg(args[1], count) || count <= 0) return fail("invalid count: " + args[1]); + } + int before = ctx.inventory.totalItems(); + ctx.inventory.insert(item, count); + int given = ctx.inventory.totalItems() - before; + if (given <= 0) return fail("no room in inventory"); + return okResult("gave " + std::to_string(given) + " " + itemDisplayName(item), given); +} + +long long timeKeyword(const std::string& word, bool& ok) { + ok = true; + if (word == "day") return 1000; + if (word == "noon") return 6000; + if (word == "night") return 13000; + if (word == "midnight") return 18000; + ok = false; + return 0; +} + +CommandResult cmdTime(CommandContext& ctx, const std::vector& args) { + if (args.size() != 2u) return fail("usage: time "); + std::string sub = toLower(args[0]); + if (sub == "set") { + bool keywordOk = false; + long long value = timeKeyword(toLower(args[1]), keywordOk); + if (!keywordOk) { + int parsed = 0; + if (!parseIntArg(args[1], parsed) || parsed < 0) return fail("invalid time: " + args[1]); + value = parsed; + } + ctx.time.setTick(value); + return okResult("time set to " + std::to_string(ctx.time.timeOfDayTicks()), 0); + } + if (sub == "add") { + int amount = 0; + if (!parseIntArg(args[1], amount) || amount <= 0) return fail("invalid amount: " + args[1]); + ctx.time.advance(amount); + return okResult("time is now " + std::to_string(ctx.time.timeOfDayTicks()), 0); + } + return fail("unknown time subcommand: " + args[0]); +} + +CommandResult cmdWeather(CommandContext& ctx, const std::vector& args) { + if (args.size() != 1u && args.size() != 2u) + return fail("usage: weather [duration]"); + std::string kind = toLower(args[0]); + WeatherState state; + if (kind == "clear") state = WeatherState::Clear; + else if (kind == "rain") state = WeatherState::Rain; + else if (kind == "thunder") state = WeatherState::Thunder; + else return fail("unknown weather: " + args[0]); + + long long duration = 6000; + if (args.size() == 2u) { + int parsed = 0; + if (!parseIntArg(args[1], parsed) || parsed <= 0) return fail("invalid duration: " + args[1]); + duration = parsed; + } + + ctx.weatherForced = true; + ctx.forcedWeather = state; + ctx.forcedWeatherDuration = duration; + if (state == WeatherState::Clear) + ctx.weather = Weather(ctx.seed, ctx.weather.settings()); + + return okResult(std::string("weather set to ") + weatherStateName(state), 0); +} + +CommandResult cmdSummon(CommandContext& ctx, const std::vector& args) { + if (args.size() != 4u) return fail("usage: summon "); + Species species; + if (!parseSpeciesName(args[0], species)) return fail("unknown mob: " + args[0]); + Vec3i cell; + if (!parseCorner(args, 1, callerBlock(ctx), cell)) return fail("invalid coordinates"); + + Vec3 position{static_cast(cell.x) + 0.5f, static_cast(cell.y), + static_cast(cell.z) + 0.5f}; + uint32_t creatureSeed = structureHash(ctx.seed, cell.x, cell.y, cell.z); + ctx.creatures.push_back(spawnCreature(species, position, creatureSeed)); + + CommandResult out = okResult(std::string("summoned ") + speciesDisplayName(species) + " at " + + posText(cell), + 1); + out.hasPosition = true; + out.position = cell; + return out; +} + +CommandResult cmdTp(CommandContext& ctx, const std::vector& args) { + if (args.size() != 3u) return fail("usage: tp "); + Vec3i target; + if (!parseCorner(args, 0, callerBlock(ctx), target)) return fail("invalid coordinates"); + ctx.callerPos = {static_cast(target.x), static_cast(target.y), + static_cast(target.z)}; + CommandResult out = okResult("teleported to " + posText(target), 1); + out.hasPosition = true; + out.position = target; + return out; +} + +CommandResult cmdKill(CommandContext& ctx, const std::vector& args) { + if (args.size() != 1u) return fail("usage: kill <@s|@p|@e|@a>"); + Selector selector; + if (!parseSelector(args[0], selector)) return fail("invalid selector: " + args[0]); + + if (selector == Selector::Self || selector == Selector::AllPlayers) { + applyDamage(ctx.player, DamageSource::Generic, ctx.player.health + 1.0f); + ctx.player.alive = false; + return okResult("killed player", 1); + } + if (selector == Selector::Nearest) { + int index = nearestCreature(ctx); + if (index < 0) return okResult("no entity to kill", 0); + Creature& creature = ctx.creatures[static_cast(index)]; + damageCreature(creature, creature.health + 1.0f); + creature.alive = false; + return okResult("killed nearest entity", 1); + } + int killed = 0; + for (Creature& creature : ctx.creatures) { + if (!creature.alive) continue; + damageCreature(creature, creature.health + 1.0f); + creature.alive = false; + ++killed; + } + return okResult("killed " + std::to_string(killed) + " entities", killed); +} + +CommandResult cmdHeal(CommandContext& ctx, const std::vector& args) { + if (args.size() > 1u) return fail("usage: heal [@s|@p|@e|@a]"); + Selector selector = Selector::Self; + if (args.size() == 1u && !parseSelector(args[0], selector)) + return fail("invalid selector: " + args[0]); + + if (selector == Selector::Self || selector == Selector::AllPlayers) { + SurvivalConfig config; + ctx.player.health = config.maxHealth; + ctx.player.alive = true; + return okResult("healed player", 1); + } + if (selector == Selector::Nearest) { + int index = nearestCreature(ctx); + if (index < 0) return okResult("no entity to heal", 0); + Creature& creature = ctx.creatures[static_cast(index)]; + creature.health = speciesStats(creature.species).maxHealth; + return okResult("healed nearest entity", 1); + } + int healed = 0; + for (Creature& creature : ctx.creatures) { + if (!creature.alive) continue; + creature.health = speciesStats(creature.species).maxHealth; + ++healed; + } + return okResult("healed " + std::to_string(healed) + " entities", healed); +} + +CommandResult cmdLocate(CommandContext& ctx, const std::vector& args) { + if (args.size() != 1u) return fail("usage: locate "); + StructureGenKind wanted; + if (!parseStructureName(args[0], wanted)) return fail("unknown structure: " + args[0]); + + StructureGenParams params; + params.seed = ctx.seed; + StructureGen gen(params); + + Vec3i origin = callerBlock(ctx); + int originChunkX = floorDiv(origin.x, chunkSize); + int originChunkZ = floorDiv(origin.z, chunkSize); + + int bestChunkX = 0; + int bestChunkZ = 0; + long long bestDistSq = 0; + bool found = false; + + for (int ring = 0; ring <= ctx.locateChunkRadius && !found; ++ring) { + for (int dz = -ring; dz <= ring; ++dz) + for (int dx = -ring; dx <= ring; ++dx) { + if (std::max(std::abs(dx), std::abs(dz)) != ring) continue; + int cx = originChunkX + dx; + int cz = originChunkZ + dz; + StructureGenKind kind; + if (!gen.structureForChunk(cx, cz, kind) || kind != wanted) continue; + long long ddx = dx; + long long ddz = dz; + long long distSq = ddx * ddx + ddz * ddz; + if (!found || distSq < bestDistSq) { + found = true; + bestDistSq = distSq; + bestChunkX = cx; + bestChunkZ = cz; + } + } + } + + if (!found) return fail("could not find " + std::string(structureDisplayName(wanted))); + + Vec3i located{bestChunkX * chunkSize + chunkSize / 2, ctx.worldMinY + 64, + bestChunkZ * chunkSize + chunkSize / 2}; + CommandResult out = okResult(std::string("nearest ") + structureDisplayName(wanted) + " at " + + posText(located), + 0); + out.hasPosition = true; + out.position = located; + return out; +} + +CommandResult cmdGamerule(CommandContext& ctx, const std::vector& args) { + if (args.size() != 1u && args.size() != 2u) return fail("usage: gamerule [value]"); + const std::string& name = args[0]; + const GameRuleValue* rule = ctx.rules.find(name); + if (!rule) return fail("unknown gamerule: " + name); + + if (args.size() == 1u) { + std::string value = rule->type == GameRuleType::Bool + ? (rule->boolValue ? "true" : "false") + : std::to_string(rule->intValue); + return okResult(name + " = " + value, 0); + } + + if (rule->type == GameRuleType::Bool) { + std::string value = toLower(args[1]); + if (value != "true" && value != "false") return fail("expected true or false"); + ctx.rules.setBool(name, value == "true"); + return okResult(name + " set to " + value, 0); + } + + int parsed = 0; + if (!parseIntArg(args[1], parsed)) return fail("expected integer"); + ctx.rules.setInt(name, parsed); + return okResult(name + " set to " + std::to_string(parsed), 0); +} + +CommandResult cmdSeed(CommandContext& ctx, const std::vector& args) { + if (!args.empty()) return fail("usage: seed"); + return okResult("seed: " + std::to_string(ctx.seed), 0); +} + +} // namespace + +std::vector tokenizeCommand(const std::string& line) { + std::vector tokens; + std::size_t i = 0; + while (i < line.size()) { + while (i < line.size() && std::isspace(static_cast(line[i]))) ++i; + if (i >= line.size()) break; + std::size_t start = i; + while (i < line.size() && !std::isspace(static_cast(line[i]))) ++i; + tokens.push_back(line.substr(start, i - start)); + } + return tokens; +} + +bool parseIntArg(const std::string& token, int& out) { + if (token.empty()) return false; + std::size_t start = 0; + bool negative = false; + if (token[0] == '+' || token[0] == '-') { + negative = token[0] == '-'; + start = 1; + } + if (start >= token.size()) return false; + long long value = 0; + for (std::size_t i = start; i < token.size(); ++i) { + if (token[i] < '0' || token[i] > '9') return false; + value = value * 10 + (token[i] - '0'); + if (value > 4000000000LL) return false; + } + out = static_cast(negative ? -value : value); + return true; +} + +bool parseCoordArg(const std::string& token, int base, int& out) { + if (token.empty()) return false; + if (token[0] == '~') { + if (token.size() == 1u) { + out = base; + return true; + } + int offset = 0; + if (!parseIntArg(token.substr(1), offset)) return false; + out = base + offset; + return true; + } + return parseIntArg(token, out); +} + +bool parseBlockName(const std::string& token, Block& out) { + std::string lowered = toLower(token); + for (const NamedBlock& entry : blockTable) + if (namesMatch(entry.name, lowered)) { + out = entry.block; + return true; + } + return false; +} + +bool parseItemName(const std::string& token, ItemId& out) { + std::string lowered = toLower(token); + for (const NamedItem& entry : itemTable) + if (namesMatch(entry.name, lowered)) { + out = entry.id; + return true; + } + return false; +} + +bool parseSpeciesName(const std::string& token, Species& out) { + std::string lowered = toLower(token); + for (const NamedSpecies& entry : speciesTable) + if (namesMatch(entry.name, lowered)) { + out = entry.species; + return true; + } + return false; +} + +bool parseStructureName(const std::string& token, StructureGenKind& out) { + std::string lowered = toLower(token); + for (const NamedStructure& entry : structureTable) + if (namesMatch(entry.name, lowered)) { + out = entry.kind; + return true; + } + return false; +} + +const char* blockDisplayName(Block block) { + for (const NamedBlock& entry : blockTable) + if (entry.block == block) return entry.name; + return "unknown"; +} + +const char* itemDisplayName(ItemId item) { + for (const NamedItem& entry : itemTable) + if (entry.id == item) return entry.name; + return "unknown"; +} + +const char* speciesDisplayName(Species species) { + for (const NamedSpecies& entry : speciesTable) + if (entry.species == species) return entry.name; + return "unknown"; +} + +const char* structureDisplayName(StructureGenKind kind) { + for (const NamedStructure& entry : structureTable) + if (entry.kind == kind) return entry.name; + return "unknown"; +} + +GameRules::GameRules() { + define("doDaylightCycle", GameRuleValue{GameRuleType::Bool, true, 0}); + define("doWeatherCycle", GameRuleValue{GameRuleType::Bool, true, 0}); + define("doMobSpawning", GameRuleValue{GameRuleType::Bool, true, 0}); + define("mobGriefing", GameRuleValue{GameRuleType::Bool, true, 0}); + define("keepInventory", GameRuleValue{GameRuleType::Bool, false, 0}); + define("naturalRegeneration", GameRuleValue{GameRuleType::Bool, true, 0}); + define("doFireTick", GameRuleValue{GameRuleType::Bool, true, 0}); + define("randomTickSpeed", GameRuleValue{GameRuleType::Int, false, 3}); + define("spawnRadius", GameRuleValue{GameRuleType::Int, false, 10}); + define("maxEntityCramming", GameRuleValue{GameRuleType::Int, false, 24}); +} + +void GameRules::define(const std::string& name, const GameRuleValue& value) { + rules[name] = value; + order.push_back(name); +} + +bool GameRules::has(const std::string& name) const { return rules.find(name) != rules.end(); } + +const GameRuleValue* GameRules::find(const std::string& name) const { + auto it = rules.find(name); + return it == rules.end() ? nullptr : &it->second; +} + +bool GameRules::boolValue(const std::string& name) const { + const GameRuleValue* rule = find(name); + return rule && rule->boolValue; +} + +int GameRules::intValue(const std::string& name) const { + const GameRuleValue* rule = find(name); + return rule ? rule->intValue : 0; +} + +bool GameRules::setBool(const std::string& name, bool value) { + auto it = rules.find(name); + if (it == rules.end() || it->second.type != GameRuleType::Bool) return false; + it->second.boolValue = value; + return true; +} + +bool GameRules::setInt(const std::string& name, int value) { + auto it = rules.find(name); + if (it == rules.end() || it->second.type != GameRuleType::Int) return false; + it->second.intValue = value; + return true; +} + +CommandDispatcher::CommandDispatcher() { + registerCommand("setblock", cmdSetblock); + registerCommand("fill", cmdFill); + registerCommand("replace", cmdReplace); + registerCommand("clone", cmdClone); + registerCommand("give", cmdGive); + registerCommand("time", cmdTime); + registerCommand("weather", cmdWeather); + registerCommand("summon", cmdSummon); + registerCommand("tp", cmdTp); + registerCommand("kill", cmdKill); + registerCommand("heal", cmdHeal); + registerCommand("locate", cmdLocate); + registerCommand("gamerule", cmdGamerule); + registerCommand("seed", cmdSeed); +} + +void CommandDispatcher::registerCommand(const std::string& name, CommandHandler handler) { + handlers[name] = std::move(handler); +} + +bool CommandDispatcher::hasCommand(const std::string& name) const { + return handlers.find(name) != handlers.end(); +} + +std::vector CommandDispatcher::commandNames() const { + std::vector names; + names.reserve(handlers.size()); + for (const auto& entry : handlers) names.push_back(entry.first); + std::sort(names.begin(), names.end()); + return names; +} + +CommandResult CommandDispatcher::execute(CommandContext& context, const std::string& line) const { + std::vector tokens = tokenizeCommand(line); + if (tokens.empty()) return fail("empty command"); + + std::string name = tokens[0]; + if (!name.empty() && name[0] == '/') name = name.substr(1); + + auto handler = handlers.find(name); + if (handler == handlers.end()) return fail("unknown command: " + name); + + std::vector args(tokens.begin() + 1, tokens.end()); + return handler->second(context, args); +} + +} // namespace vf