778 lines
31 KiB
C++
778 lines
31 KiB
C++
#include "vf/Command.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<char>(std::tolower(static_cast<unsigned char>(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<int>(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<std::string>& 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<long long>(hi.x - lo.x) + 1;
|
|
long long dy = static_cast<long long>(hi.y - lo.y) + 1;
|
|
long long dz = static_cast<long long>(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<int>(i);
|
|
bestDistSq = distSq;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
CommandResult cmdSetblock(CommandContext& ctx, const std::vector<std::string>& args) {
|
|
if (args.size() != 4u) return fail("usage: setblock <x> <y> <z> <block>");
|
|
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<std::string>& args) {
|
|
if (args.size() != 7u && args.size() != 8u)
|
|
return fail("usage: fill <x1> <y1> <z1> <x2> <y2> <z2> <block> [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<std::string>& args) {
|
|
if (args.size() != 8u)
|
|
return fail("usage: replace <x1> <y1> <z1> <x2> <y2> <z2> <from> <to>");
|
|
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<std::string>& args) {
|
|
if (args.size() != 9u)
|
|
return fail("usage: clone <x1> <y1> <z1> <x2> <y2> <z2> <dx> <dy> <dz>");
|
|
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<Block> buffer;
|
|
buffer.reserve(static_cast<std::size_t>(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<std::string>& args) {
|
|
if (args.size() != 1u && args.size() != 2u) return fail("usage: give <item> [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<std::string>& args) {
|
|
if (args.size() != 2u) return fail("usage: time <set|add> <ticks|day|noon|night|midnight>");
|
|
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<std::string>& args) {
|
|
if (args.size() != 1u && args.size() != 2u)
|
|
return fail("usage: weather <clear|rain|thunder> [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<std::string>& args) {
|
|
if (args.size() != 4u) return fail("usage: summon <mob> <x> <y> <z>");
|
|
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<float>(cell.x) + 0.5f, static_cast<float>(cell.y),
|
|
static_cast<float>(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<std::string>& args) {
|
|
if (args.size() != 3u) return fail("usage: tp <x> <y> <z>");
|
|
Vec3i target;
|
|
if (!parseCorner(args, 0, callerBlock(ctx), target)) return fail("invalid coordinates");
|
|
ctx.callerPos = {static_cast<float>(target.x), static_cast<float>(target.y),
|
|
static_cast<float>(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<std::string>& 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<std::size_t>(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<std::string>& 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<std::size_t>(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<std::string>& args) {
|
|
if (args.size() != 1u) return fail("usage: locate <structure>");
|
|
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<std::string>& args) {
|
|
if (args.size() != 1u && args.size() != 2u) return fail("usage: gamerule <name> [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<std::string>& args) {
|
|
if (!args.empty()) return fail("usage: seed");
|
|
return okResult("seed: " + std::to_string(ctx.seed), 0);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::vector<std::string> tokenizeCommand(const std::string& line) {
|
|
std::vector<std::string> tokens;
|
|
std::size_t i = 0;
|
|
while (i < line.size()) {
|
|
while (i < line.size() && std::isspace(static_cast<unsigned char>(line[i]))) ++i;
|
|
if (i >= line.size()) break;
|
|
std::size_t start = i;
|
|
while (i < line.size() && !std::isspace(static_cast<unsigned char>(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<int>(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<std::string> CommandDispatcher::commandNames() const {
|
|
std::vector<std::string> 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<std::string> 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<std::string> args(tokens.begin() + 1, tokens.end());
|
|
return handler->second(context, args);
|
|
}
|
|
|
|
} // namespace vf
|