Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/utility
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/utility
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/utility')
-rw-r--r--source/utility/CMakeLists.txt77
-rw-r--r--source/utility/asset_packer.cpp79
-rw-r--r--source/utility/asset_unpacker.cpp53
-rw-r--r--source/utility/dump_versioned_json.cpp20
-rw-r--r--source/utility/dungeon_generation_benchmark.cpp61
-rw-r--r--source/utility/fix_embedded_tilesets.cpp101
-rw-r--r--source/utility/game_repl.cpp53
-rw-r--r--source/utility/generation_benchmark.cpp81
-rw-r--r--source/utility/make_versioned_json.cpp20
-rw-r--r--source/utility/map_grep.cpp116
-rw-r--r--source/utility/planet_mapgen.cpp117
-rw-r--r--source/utility/render_terrain_selector.cpp96
-rw-r--r--source/utility/tileset_updater.cpp326
-rw-r--r--source/utility/tileset_updater.hpp102
-rw-r--r--source/utility/update_tilesets.cpp262
-rw-r--r--source/utility/word_count.cpp191
-rw-r--r--source/utility/world_benchmark.cpp101
17 files changed, 1856 insertions, 0 deletions
diff --git a/source/utility/CMakeLists.txt b/source/utility/CMakeLists.txt
new file mode 100644
index 0000000..4047ff8
--- /dev/null
+++ b/source/utility/CMakeLists.txt
@@ -0,0 +1,77 @@
+INCLUDE_DIRECTORIES (
+ ${STAR_EXTERN_INCLUDES}
+ ${STAR_CORE_INCLUDES}
+ ${STAR_BASE_INCLUDES}
+ ${STAR_PLATFORM_INCLUDES}
+ ${STAR_GAME_INCLUDES}
+ )
+
+ADD_EXECUTABLE (asset_packer
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base>
+ asset_packer.cpp)
+TARGET_LINK_LIBRARIES (asset_packer ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (asset_unpacker
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base>
+ asset_unpacker.cpp)
+TARGET_LINK_LIBRARIES (asset_unpacker ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (dump_versioned_json
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ dump_versioned_json.cpp)
+TARGET_LINK_LIBRARIES (dump_versioned_json ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (game_repl
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ game_repl.cpp)
+TARGET_LINK_LIBRARIES (game_repl ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (make_versioned_json
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ make_versioned_json.cpp)
+TARGET_LINK_LIBRARIES (make_versioned_json ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (planet_mapgen
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ planet_mapgen.cpp)
+TARGET_LINK_LIBRARIES (planet_mapgen ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (render_terrain_selector
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ render_terrain_selector.cpp)
+TARGET_LINK_LIBRARIES (render_terrain_selector ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (update_tilesets
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ update_tilesets.cpp tileset_updater.cpp)
+TARGET_LINK_LIBRARIES (update_tilesets ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (fix_embedded_tilesets
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ fix_embedded_tilesets.cpp)
+TARGET_LINK_LIBRARIES (fix_embedded_tilesets ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (world_benchmark
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ world_benchmark.cpp)
+TARGET_LINK_LIBRARIES (world_benchmark ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (generation_benchmark
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ generation_benchmark.cpp)
+TARGET_LINK_LIBRARIES (generation_benchmark ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (dungeon_generation_benchmark
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ dungeon_generation_benchmark.cpp)
+TARGET_LINK_LIBRARIES (dungeon_generation_benchmark ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (map_grep
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ map_grep.cpp)
+TARGET_LINK_LIBRARIES (map_grep ${STAR_EXT_LIBS})
+
+ADD_EXECUTABLE (word_count
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
+ word_count.cpp)
+TARGET_LINK_LIBRARIES (word_count ${STAR_EXT_LIBS})
diff --git a/source/utility/asset_packer.cpp b/source/utility/asset_packer.cpp
new file mode 100644
index 0000000..7777264
--- /dev/null
+++ b/source/utility/asset_packer.cpp
@@ -0,0 +1,79 @@
+#include "StarPackedAssetSource.hpp"
+#include "StarTime.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarFile.hpp"
+#include "StarVersionOptionParser.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ double startTime = Time::monotonicTime();
+
+ VersionOptionParser optParse;
+ optParse.setSummary("Packs asset folder into a starbound .pak file");
+ optParse.addParameter("c", "configFile", OptionParser::Optional, "JSON file with ignore lists and ordering info");
+ optParse.addSwitch("s", "Enable server mode");
+ optParse.addSwitch("v", "Verbose, list each file added");
+ optParse.addArgument("assets folder path", OptionParser::Required, "Path to the assets to be packed");
+ optParse.addArgument("output filename", OptionParser::Required, "Output pak file");
+
+ auto opts = optParse.commandParseOrDie(argc, argv);
+
+ String assetsFolderPath = opts.arguments.at(0);
+ String outputFilename = opts.arguments.at(1);
+
+ StringList ignoreFiles;
+ StringList extensionOrdering;
+ if (opts.parameters.contains("c")) {
+ String configFile = opts.parameters.get("c").first();
+ String configFileContents;
+ try {
+ configFileContents = File::readFileString(configFile);
+ } catch (IOException const& e) {
+ cerrf("Could not open specified configFile: %s\n", configFile);
+ cerrf("For the following reason: %s\n", outputException(e, false));
+ return 1;
+ }
+
+ Json configFileJson;
+ try {
+ configFileJson = Json::parseJson(configFileContents);
+ } catch (JsonParsingException const& e) {
+ cerrf("Could not parse the specified configFile: %s\n", configFile);
+ cerrf("For the following reason: %s\n", outputException(e, false));
+ return 1;
+ }
+
+ try {
+ ignoreFiles = jsonToStringList(configFileJson.get("globalIgnore", JsonArray()));
+ if (opts.switches.contains("s"))
+ ignoreFiles.appendAll(jsonToStringList(configFileJson.get("serverIgnore", JsonArray())));
+ extensionOrdering = jsonToStringList(configFileJson.get("extensionOrdering", JsonArray()));
+ } catch (JsonException const& e) {
+ cerrf("Could not read the asset_packer config file %s\n", configFile);
+ cerrf("%s\n", outputException(e, false));
+ return 1;
+ }
+ }
+
+ bool verbose = opts.parameters.contains("v");
+
+ function<void(size_t, size_t, String, String, bool)> BuildProgressCallback;
+ auto progressCallback = [verbose](size_t, size_t, String filePath, String assetPath) {
+ if (verbose)
+ coutf("Adding file '%s' to the target pak as '%s'\n", filePath, assetPath);
+ };
+
+ outputFilename = File::relativeTo(File::fullPath(File::dirName(outputFilename)), File::baseName(outputFilename));
+ DirectoryAssetSource directorySource(assetsFolderPath, ignoreFiles);
+ PackedAssetSource::build(directorySource, outputFilename, extensionOrdering, progressCallback);
+
+ coutf("Output packed assets to %s in %ss\n", outputFilename, Time::monotonicTime() - startTime);
+ return 0;
+
+ } catch (std::exception const& e) {
+ cerrf("Exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/asset_unpacker.cpp b/source/utility/asset_unpacker.cpp
new file mode 100644
index 0000000..d4d4f6d
--- /dev/null
+++ b/source/utility/asset_unpacker.cpp
@@ -0,0 +1,53 @@
+#include "StarPackedAssetSource.hpp"
+#include "StarTime.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarFile.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ double startTime = Time::monotonicTime();
+
+ if (argc != 3) {
+ cerrf("Usage: %s <assets pak path> <target output directory>\n", argv[0]);
+ cerrf("If the target output directory does not exist it will be created\n");
+ return 1;
+ }
+
+ String inputFile = argv[1];
+ String outputFolderPath = argv[2];
+
+ PackedAssetSource assetsPack(inputFile);
+
+ if (!File::isDirectory(outputFolderPath))
+ File::makeDirectory(outputFolderPath);
+
+ File::changeDirectory(outputFolderPath);
+
+ auto allFiles = assetsPack.assetPaths();
+
+ for (auto file : allFiles) {
+ try {
+ auto fileData = assetsPack.read(file);
+ auto relativePath = "." + file;
+ auto relativeDir = File::dirName(relativePath);
+ File::makeDirectoryRecursive(relativeDir);
+ File::writeFile(fileData, relativePath);
+ } catch (AssetSourceException const& e) {
+ cerrf("Could not open file: %s\n", file);
+ cerrf("Reason: %s\n", outputException(e, false));
+ }
+ }
+
+ auto metadata = assetsPack.metadata();
+ if (!metadata.empty())
+ File::writeFile(Json(move(metadata)).printJson(2), "_metadata");
+
+ coutf("Unpacked assets to %s in %ss\n", outputFolderPath, Time::monotonicTime() - startTime);
+ return 0;
+ } catch (std::exception const& e) {
+ cerrf("Exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/dump_versioned_json.cpp b/source/utility/dump_versioned_json.cpp
new file mode 100644
index 0000000..2672548
--- /dev/null
+++ b/source/utility/dump_versioned_json.cpp
@@ -0,0 +1,20 @@
+#include "StarFile.hpp"
+#include "StarVersioningDatabase.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ if (argc != 3) {
+ coutf("Usage, %s <versioned_json_binary> <versioned_json_json>\n", argv[0]);
+ return -1;
+ }
+
+ auto versionedJson = VersionedJson::readFile(argv[1]);
+ File::writeFile(versionedJson.toJson().printJson(2), argv[2]);
+ return 0;
+ } catch (std::exception const& e) {
+ coutf("Error! Caught exception %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/dungeon_generation_benchmark.cpp b/source/utility/dungeon_generation_benchmark.cpp
new file mode 100644
index 0000000..a67cb02
--- /dev/null
+++ b/source/utility/dungeon_generation_benchmark.cpp
@@ -0,0 +1,61 @@
+#include "StarRootLoader.hpp"
+#include "StarCelestialDatabase.hpp"
+#include "StarWorldTemplate.hpp"
+#include "StarWorldServer.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ unsigned repetitions = 5;
+ unsigned reportEvery = 1;
+ String dungeonWorldName = "outpost";
+
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}});
+ rootLoader.addParameter("dungeonWorld", "dungeonWorld", OptionParser::Optional, strf("dungeonWorld to test, default is %s", dungeonWorldName));
+ rootLoader.addParameter("repetitions", "repetitions", OptionParser::Optional, strf("number of times to generate, default %s", repetitions));
+ rootLoader.addParameter("reportevery", "report repetitions", OptionParser::Optional, strf("number of repetitions before each progress report, default %s", reportEvery));
+
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ coutf("Fully loading root...");
+ root->fullyLoad();
+ coutf(" done\n");
+
+ if (auto repetitionsOption = options.parameters.maybe("repetitions"))
+ repetitions = lexicalCast<unsigned>(repetitionsOption->first());
+
+ if (auto reportEveryOption = options.parameters.maybe("reportevery"))
+ reportEvery = lexicalCast<unsigned>(reportEveryOption->first());
+
+ if (auto dungeonWorldOption = options.parameters.maybe("dungeonWorld"))
+ dungeonWorldName = dungeonWorldOption->first();
+
+ double start = Time::monotonicTime();
+ double lastReport = Time::monotonicTime();
+
+ coutf("testing %s generations of dungeonWorld %s\n", repetitions, dungeonWorldName);
+
+ for (unsigned i = 0; i < repetitions; ++i) {
+ if (i > 0 && i % reportEvery == 0) {
+ float gps = reportEvery / (Time::monotonicTime() - lastReport);
+ lastReport = Time::monotonicTime();
+ coutf("[%s] %ss | Generations Per Second: %s\n", i, Time::monotonicTime() - start, gps);
+ }
+
+ VisitableWorldParametersPtr worldParameters = generateFloatingDungeonWorldParameters(dungeonWorldName);
+ auto worldTemplate = make_shared<WorldTemplate>(worldParameters, SkyParameters(), 1234);
+ WorldServer worldServer(move(worldTemplate), File::ephemeralFile());
+ }
+
+ coutf("Finished %s generations of dungeonWorld %s in %s seconds", repetitions, dungeonWorldName, Time::monotonicTime() - start);
+
+ return 0;
+
+ } catch (std::exception const& e) {
+ cerrf("Exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/fix_embedded_tilesets.cpp b/source/utility/fix_embedded_tilesets.cpp
new file mode 100644
index 0000000..262426d
--- /dev/null
+++ b/source/utility/fix_embedded_tilesets.cpp
@@ -0,0 +1,101 @@
+#include "StarFile.hpp"
+#include "StarLogging.hpp"
+#include "StarRootLoader.hpp"
+#include "StarTilesetDatabase.hpp"
+
+using namespace Star;
+
+void removeCommonPrefix(StringList& a, StringList& b) {
+ // Remove elements from a and b until there is one that differs.
+ while (a.size() > 0 && b.size() > 0 && a[0] == b[0]) {
+ a.eraseAt(0);
+ b.eraseAt(0);
+ }
+}
+
+String createRelativePath(String fromFile, String toFile) {
+ if (!File::isDirectory(fromFile))
+ fromFile = File::dirName(fromFile);
+ fromFile = File::fullPath(fromFile);
+ toFile = File::fullPath(toFile);
+
+ StringList fromParts = fromFile.splitAny("/\\");
+ StringList toParts = toFile.splitAny("/\\");
+ removeCommonPrefix(fromParts, toParts);
+
+ StringList relativeParts;
+ for (String part : fromParts)
+ relativeParts.append("..");
+ relativeParts.appendAll(toParts);
+
+ return relativeParts.join("/");
+}
+
+Maybe<Json> repairTileset(Json tileset, String const& mapPath, String const& tilesetPath) {
+ if (tileset.contains("source"))
+ return {};
+ size_t firstGid = tileset.getUInt("firstgid");
+ String tilesetName = tileset.getString("name");
+ String tilesetFileName = File::relativeTo(tilesetPath, tilesetName + ".json");
+ if (!File::exists(tilesetFileName))
+ throw StarException::format("Tileset %s does not exist. Can't repair %s", tilesetFileName, mapPath);
+ return {JsonObject{{"firstgid", firstGid}, {"source", createRelativePath(mapPath, tilesetFileName)}}};
+}
+
+Maybe<Json> repair(Json mapJson, String const& mapPath, String const& tilesetPath) {
+ JsonArray tilesets = mapJson.getArray("tilesets");
+ bool changed = false;
+ for (size_t i = 0; i < tilesets.size(); ++i) {
+ if (Maybe<Json> tileset = repairTileset(tilesets[i], mapPath, tilesetPath)) {
+ tilesets[i] = *tileset;
+ changed = true;
+ }
+ }
+ if (!changed)
+ return {};
+ return mapJson.set("tilesets", tilesets);
+}
+
+void forEachRecursiveFileMatch(String const& dirName, String const& filenameSuffix, function<void(String)> func) {
+ for (pair<String, bool> entry : File::dirList(dirName)) {
+ if (entry.second)
+ forEachRecursiveFileMatch(File::relativeTo(dirName, entry.first), filenameSuffix, func);
+ else if (entry.first.endsWith(filenameSuffix))
+ func(File::relativeTo(dirName, entry.first));
+ }
+}
+
+void fixEmbeddedTilesets(String const& searchRoot, String const& tilesetPath) {
+ forEachRecursiveFileMatch(searchRoot, ".json", [tilesetPath](String const& path) {
+ Json json = Json::parseJson(File::readFileString(path));
+ if (json.contains("tilesets")) {
+ if (Maybe<Json> fixed = repair(json, path, tilesetPath)) {
+ File::writeFile(fixed->repr(2, true), path);
+ Logger::info("Repaired %s", path);
+ }
+ }
+ });
+}
+
+int main(int argc, char* argv[]) {
+ try {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Info, false, {}});
+ rootLoader.setSummary("Replaces embedded tilesets in Tiled JSON files with references to external tilesets. Assumes tilesets are available in the packed assets.");
+ rootLoader.addArgument("searchRoot", OptionParser::Required);
+ rootLoader.addArgument("tilesetsPath", OptionParser::Required);
+
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ String searchRoot = options.arguments[0];
+ String tilesetPath = options.arguments[1];
+
+ fixEmbeddedTilesets(searchRoot, tilesetPath);
+
+ return 0;
+ } catch (std::exception const& e) {
+ cerrf("exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/game_repl.cpp b/source/utility/game_repl.cpp
new file mode 100644
index 0000000..e52fb07
--- /dev/null
+++ b/source/utility/game_repl.cpp
@@ -0,0 +1,53 @@
+#include "StarRootLoader.hpp"
+#include "StarRootLuaBindings.hpp"
+#include "StarUtilityLuaBindings.hpp"
+#include "StarRootLuaBindings.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}});
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ auto engine = LuaEngine::create(true);
+ auto context = engine->createContext();
+ context.setCallbacks("sb", LuaBindings::makeUtilityCallbacks());
+ context.setCallbacks("root", LuaBindings::makeRootCallbacks());
+
+ String code;
+ bool continuation = false;
+ while (!std::cin.eof()) {
+ auto getline = [](std::istream& stream) -> String {
+ std::string line;
+ std::getline(stream, line);
+ return String(move(line));
+ };
+
+ if (continuation) {
+ std::cout << ">> ";
+ std::cout.flush();
+ code += getline(std::cin);
+ code += '\n';
+ } else {
+ std::cout << "> ";
+ std::cout.flush();
+ code = getline(std::cin);
+ code += '\n';
+ }
+
+ try {
+ auto result = context.eval<LuaVariadic<LuaValue>>(code);
+ for (auto r : result)
+ coutf("%s\n", r);
+ continuation = false;
+ } catch (LuaIncompleteStatementException const&) {
+ continuation = true;
+ } catch (std::exception const& e) {
+ coutf("Error: %s\n", outputException(e, false));
+ continuation = false;
+ }
+ }
+ return 0;
+}
diff --git a/source/utility/generation_benchmark.cpp b/source/utility/generation_benchmark.cpp
new file mode 100644
index 0000000..f3f2f97
--- /dev/null
+++ b/source/utility/generation_benchmark.cpp
@@ -0,0 +1,81 @@
+#include "StarRootLoader.hpp"
+#include "StarCelestialDatabase.hpp"
+#include "StarWorldTemplate.hpp"
+#include "StarWorldServer.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}});
+ rootLoader.addParameter("coordinate", "coordinate", OptionParser::Optional, "world coordinate to test");
+ rootLoader.addParameter("regions", "regions", OptionParser::Optional, "number of regions to generate, default 1000");
+ rootLoader.addParameter("regionsize", "size", OptionParser::Optional, "width / height of each generation region, default 10");
+ rootLoader.addParameter("reportevery", "report regions", OptionParser::Optional, "number of generation regions before each progress report, default 20");
+
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ coutf("Fully loading root...");
+ root->fullyLoad();
+ coutf(" done\n");
+
+ CelestialMasterDatabase celestialDatabase;
+
+ CelestialCoordinate coordinate;
+ if (auto coordinateOption = options.parameters.maybe("coordinate")) {
+ coordinate = CelestialCoordinate(coordinateOption->first());
+ } else {
+ coordinate = celestialDatabase.findRandomWorld(100, 50, [&](CelestialCoordinate const& coord) {
+ return celestialDatabase.parameters(coord)->isVisitable();
+ }).take();
+ }
+
+ unsigned regionsToGenerate = 1000;
+ if (auto regionsOption = options.parameters.maybe("regions"))
+ regionsToGenerate = lexicalCast<unsigned>(regionsOption->first());
+
+ unsigned regionSize = 10;
+ if (auto regionSizeOption = options.parameters.maybe("regionsize"))
+ regionSize = lexicalCast<unsigned>(regionSizeOption->first());
+
+ unsigned reportEvery = 20;
+ if (auto reportEveryOption = options.parameters.maybe("reportevery"))
+ reportEvery = lexicalCast<unsigned>(reportEveryOption->first());
+
+ coutf("testing generation on coordinate %s\n", coordinate);
+
+ auto worldParameters = celestialDatabase.parameters(coordinate).take();
+ auto worldTemplate = make_shared<WorldTemplate>(worldParameters.visitableParameters(), SkyParameters(), worldParameters.seed());
+
+ auto rand = RandomSource(worldTemplate->worldSeed());
+
+ WorldServer worldServer(move(worldTemplate), File::ephemeralFile());
+ Vec2U worldSize = worldServer.geometry().size();
+
+ double start = Time::monotonicTime();
+ double lastReport = Time::monotonicTime();
+
+ coutf("Starting world generation for %s regions\n", regionsToGenerate);
+
+ for (unsigned i = 0; i < regionsToGenerate; ++i) {
+ if (i != 0 && i % reportEvery == 0) {
+ float gps = reportEvery / (Time::monotonicTime() - lastReport);
+ lastReport = Time::monotonicTime();
+ coutf("[%s] %ss | Generatons Per Second: %s\n", i, Time::monotonicTime() - start, gps);
+ }
+
+ RectI region = RectI::withCenter(Vec2I(rand.randInt(0, worldSize[0]), rand.randInt(0, worldSize[1])), Vec2I::filled(regionSize));
+ worldServer.generateRegion(region);
+ }
+
+ coutf("Finished generating %s regions with size %sx%s in world '%s' in %s seconds", regionsToGenerate, regionSize, regionSize, coordinate, Time::monotonicTime() - start);
+
+ return 0;
+
+ } catch (std::exception const& e) {
+ cerrf("Exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/make_versioned_json.cpp b/source/utility/make_versioned_json.cpp
new file mode 100644
index 0000000..560f532
--- /dev/null
+++ b/source/utility/make_versioned_json.cpp
@@ -0,0 +1,20 @@
+#include "StarFile.hpp"
+#include "StarVersioningDatabase.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ if (argc != 3) {
+ coutf("Usage, %s <versioned_json_json> <versioned_json_binary>\n", argv[0]);
+ return -1;
+ }
+
+ auto versionedJson = VersionedJson::fromJson(Json::parse(File::readFileString(argv[1])));
+ VersionedJson::writeFile(versionedJson, argv[2]);
+ return 0;
+ } catch (std::exception const& e) {
+ coutf("Error! Caught exception %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/map_grep.cpp b/source/utility/map_grep.cpp
new file mode 100644
index 0000000..03083e0
--- /dev/null
+++ b/source/utility/map_grep.cpp
@@ -0,0 +1,116 @@
+#include "StarFile.hpp"
+#include "StarLogging.hpp"
+#include "StarRootLoader.hpp"
+#include "StarDungeonTMXPart.hpp"
+
+using namespace Star;
+using namespace Star::Dungeon;
+
+typedef String TileName;
+typedef pair<String, String> TileProperty;
+
+typedef MVariant<TileName, TileProperty> MatchCriteria;
+
+struct SearchParameters {
+ MatchCriteria criteria;
+};
+
+typedef function<void(String, Vec2I)> MatchReporter;
+
+String const MapFilenameSuffix = ".json";
+
+Maybe<String> matchTile(SearchParameters const& search, Tiled::Tile const& tile) {
+ Tiled::Properties const& properties = tile.properties;
+ if (search.criteria.is<TileName>()) {
+ if (auto tileName = properties.opt<String>("//name"))
+ if (tileName->regexMatch(search.criteria.get<TileName>()))
+ return tileName;
+ } else {
+ String propertyName;
+ String matchValue;
+ tie(propertyName, matchValue) = search.criteria.get<TileProperty>();
+ if (auto propertyValue = properties.opt<String>(propertyName))
+ if (propertyValue->regexMatch(matchValue))
+ return properties.opt<String>("//name").value("?");
+ }
+ return {};
+}
+
+void grepTileLayer(SearchParameters const& search, TMXMapPtr map, TMXTileLayerPtr tileLayer, MatchReporter callback) {
+ tileLayer->forEachTile(map.get(),
+ [&](Vec2I pos, Tile const& tile) {
+ if (auto tileName = matchTile(search, static_cast<Tiled::Tile const&>(tile)))
+ callback(*tileName, pos);
+ return false;
+ });
+}
+
+void grepObjectGroup(SearchParameters const& search, TMXObjectGroupPtr objectGroup, MatchReporter callback) {
+ for (auto object : objectGroup->objects()) {
+ if (auto tileName = matchTile(search, object->tile()))
+ callback(*tileName, object->pos());
+ }
+}
+
+void grepMap(SearchParameters const& search, String file) {
+ auto map = make_shared<TMXMap>(Json::parseJson(File::readFileString(file)));
+
+ for (auto tileLayer : map->tileLayers())
+ grepTileLayer(search, map, tileLayer, [&](String const& tileName, Vec2I const& pos) {
+ coutf("%s: %s: %s @ %s\n", file, tileLayer->name(), tileName, pos);
+ });
+
+ for (auto objectGroup : map->objectGroups())
+ grepObjectGroup(search, objectGroup, [&](String const& tileName, Vec2I const& pos) {
+ coutf("%s: %s: %s @ %s\n", file, objectGroup->name(), tileName, pos);
+ });
+}
+
+void grepDirectory(SearchParameters const& search, String directory) {
+ for (pair<String, bool> entry : File::dirList(directory)) {
+ if (entry.second)
+ grepDirectory(search, File::relativeTo(directory, entry.first));
+ else if (entry.first.endsWith(MapFilenameSuffix))
+ grepMap(search, File::relativeTo(directory, entry.first));
+ }
+}
+
+void grepPath(SearchParameters const& search, String path) {
+ if (File::isFile(path)) {
+ grepMap(search, path);
+ } else if (File::isDirectory(path)) {
+ grepDirectory(search, path);
+ }
+}
+
+MatchCriteria parseMatchCriteria(String const& criteriaStr) {
+ if (criteriaStr.contains("=")) {
+ StringList parts = criteriaStr.split('=', 1);
+ return make_pair(parts[0], parts[1]);
+ }
+ return TileName(criteriaStr);
+}
+
+int main(int argc, char* argv[]) {
+ try {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Warn, false, {}});
+ rootLoader.setSummary("Search Tiled map files for specific materials or objects.");
+ rootLoader.addArgument("MaterialId|ObjectName|Property=Value", OptionParser::Required);
+ rootLoader.addArgument("JsonMapFile", OptionParser::Multiple);
+
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ SearchParameters search = {parseMatchCriteria(options.arguments[0])};
+ StringList files = options.arguments.slice(1);
+
+ for (auto file : files)
+ grepPath(search, file);
+
+ return 0;
+ } catch (std::exception const& e) {
+ cerrf("exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/planet_mapgen.cpp b/source/utility/planet_mapgen.cpp
new file mode 100644
index 0000000..7bc9f8d
--- /dev/null
+++ b/source/utility/planet_mapgen.cpp
@@ -0,0 +1,117 @@
+#include "StarFile.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarImage.hpp"
+#include "StarRootLoader.hpp"
+#include "StarCelestialDatabase.hpp"
+#include "StarWorldTemplate.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}});
+
+ rootLoader.setSummary("Generate a WorldTemplate and output the data in it to an image");
+
+ rootLoader.addParameter("coordinate", "coordinate", OptionParser::Optional, "coordinate for the celestial world");
+ rootLoader.addParameter("coordseed", "seed", OptionParser::Optional, "seed to use when selecting a random celestial world coordinate");
+ rootLoader.addParameter("size", "size", OptionParser::Optional, "x,y size of the region to be rendered");
+ rootLoader.addSwitch("weighting", "Output instead the region weighting at each point");
+ rootLoader.addSwitch("weightingblocknoise", "apply layout block noise before outputting weighting");
+ rootLoader.addSwitch("transition", "show biome transition regions");
+
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ CelestialMasterDatabasePtr celestialDatabase = make_shared<CelestialMasterDatabase>();
+
+ Maybe<CelestialCoordinate> coordinate;
+ if (!options.parameters["coordinate"].empty())
+ coordinate = CelestialCoordinate(options.parameters["coordinate"].first());
+ else if (!options.parameters["coordseed"].empty())
+ coordinate = celestialDatabase->findRandomWorld(
+ 10, 50, {}, lexicalCast<uint64_t>(options.parameters["coordseed"].first()));
+ else
+ coordinate = celestialDatabase->findRandomWorld();
+
+ if (!coordinate)
+ throw StarException("Could not find world to generate, try again");
+
+ coutf("Generating world with coordinate %s\n", *coordinate);
+
+ WorldTemplate worldTemplate(*coordinate, celestialDatabase);
+ auto size = worldTemplate.size();
+
+ if (!options.parameters["size"].empty()) {
+ auto regionSize = Vec2U(lexicalCast<unsigned>(options.parameters["size"].first().split(",")[0]),
+ lexicalCast<unsigned>(options.parameters["size"].first().split(",")[1]));
+ size = regionSize.piecewiseClamp(Vec2U(0, 0), size);
+ } else if (size[0] > 1000) {
+ size[0] = 1000;
+ }
+
+ coutf("Generating %s size image for world of type '%s'\n", size, worldTemplate.worldParameters()->typeName);
+ auto outputImage = make_shared<Image>(size, PixelFormat::RGB24);
+
+ Color groundColor = Color::rgb(255, 0, 0);
+ Color caveColor = Color::rgb(128, 0, 0);
+ Color blankColor = Color::rgb(0, 0, 0);
+
+ for (size_t x = 0; x < size[0]; ++x) {
+ for (size_t y = 0; y < size[1]; ++y) {
+ if (options.switches.contains("weighting")) {
+ auto layout = worldTemplate.worldLayout();
+ Color color = Color::Black;
+ Vec2I pos(x, y);
+ if (options.switches.contains("weightingblocknoise")) {
+ if (auto blockNoise = layout->blockNoise())
+ pos = blockNoise->apply(pos, size);
+ }
+ auto weightings = layout->getWeighting(pos[0], pos[1]);
+ for (auto const& weighting : weightings) {
+ Color mixColor = Color::rgb(128, 0, 0);
+ mixColor.setHue(staticRandomFloat((uint64_t)weighting.region));
+ color = Color::rgbaf(color.toRgbaF() + mixColor.toRgbaF() * weighting.weight);
+ }
+ outputImage->set(x, y, color.toRgb());
+ } else if (options.switches.contains("transition")) {
+ auto blockInfo = worldTemplate.blockInfo(x, y);
+ if (isRealMaterial(blockInfo.foreground)) {
+ Color color = groundColor;
+ color.setHue(blockInfo.biomeTransition ? 0 : 0.5f);
+ outputImage->set(x, y, color.toRgb());
+ } else if (isRealMaterial(blockInfo.background)) {
+ Color color = caveColor;
+ color.setHue(blockInfo.biomeTransition ? 0 : 0.5f);
+ outputImage->set(x, y, color.toRgb());
+ } else {
+ outputImage->set(x, y, blankColor.toRgb());
+ }
+ } else {
+ // Image y = 0 is the top, so reverse it for the world tile
+ auto blockInfo = worldTemplate.blockInfo(x, y);
+ if (isRealMaterial(blockInfo.foreground)) {
+ Color color = groundColor;
+ color.setHue(staticRandomFloat(blockInfo.foreground));
+ color.setSaturation(staticRandomFloat(blockInfo.foregroundMod));
+ outputImage->set(x, y, color.toRgb());
+ } else if (isRealMaterial(blockInfo.background)) {
+ Color color = caveColor;
+ color.setHue(staticRandomFloat(blockInfo.background));
+ color.setSaturation(staticRandomFloat(blockInfo.backgroundMod));
+ outputImage->set(x, y, color.toRgb());
+ } else {
+ outputImage->set(x, y, blankColor.toRgb());
+ }
+ }
+ }
+ }
+
+ outputImage->writePng(File::open("mapgen.png", IOMode::Write));
+ return 0;
+ } catch (std::exception const& e) {
+ cerrf("exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/render_terrain_selector.cpp b/source/utility/render_terrain_selector.cpp
new file mode 100644
index 0000000..a9e508a
--- /dev/null
+++ b/source/utility/render_terrain_selector.cpp
@@ -0,0 +1,96 @@
+#include "StarFile.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarImage.hpp"
+#include "StarRootLoader.hpp"
+#include "StarTerrainDatabase.hpp"
+#include "StarJson.hpp"
+#include "StarRandom.hpp"
+#include "StarColor.hpp"
+#include "StarMultiArray.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}});
+
+ rootLoader.setSummary("Generate a heatmap image visualizing the output of a given terrain selector");
+
+ rootLoader.addParameter("selector", "selector", OptionParser::Required, "name of the terrain selector to be rendered");
+ rootLoader.addParameter("size", "size", OptionParser::Required, "x,y size of the region to be rendered");
+ rootLoader.addParameter("seed", "seed", OptionParser::Optional, "seed value for the selector");
+ rootLoader.addParameter("commonality", "commonality", OptionParser::Optional, "commonality value for the selector (default 1)");
+ rootLoader.addParameter("scale", "scale", OptionParser::Optional, "maximum distance from 0 for color range");
+ rootLoader.addParameter("mode", "mode", OptionParser::Optional, "color mode: heatmap, terrain");
+
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ auto size = Vec2U(lexicalCast<unsigned>(options.parameters["size"].first().split(",")[0]), lexicalCast<unsigned>(options.parameters["size"].first().split(",")[1]));
+
+ auto seed = Random::randu64();
+ if (!options.parameters["seed"].empty())
+ seed = lexicalCast<uint64_t>(options.parameters["seed"].first());
+
+ float commonality = 1.0f;
+ if (!options.parameters["commonality"].empty())
+ commonality = lexicalCast<float>(options.parameters["commonality"].first());
+
+ float scale = 1.0f;
+ bool autoScale = true;
+ if (!options.parameters["scale"].empty()) {
+ autoScale = false;
+ scale = lexicalCast<float>(options.parameters["scale"].first());
+ }
+
+ String mode = "heatmap";
+ if (!options.parameters["mode"].empty())
+ mode = options.parameters["mode"].first();
+
+ auto selectorParameters = TerrainSelectorParameters(JsonObject{
+ {"worldWidth", size[0]},
+ {"baseHeight", size[1] / 2},
+ {"seed", seed},
+ {"commonality", commonality}
+ });
+
+ auto selector = Root::singleton().terrainDatabase()->createNamedSelector(options.parameters["selector"].first(), selectorParameters);
+
+ MultiArray<float, 2> terrainResult({size[0], size[1]}, 0.0f);
+ for (size_t x = 0; x < size[0]; ++x) {
+ for (size_t y = 0; y < size[1]; ++y) {
+ auto value = selector->get(x, y);
+ terrainResult(x, y) = value;
+ if (autoScale)
+ scale = max(scale, abs(value));
+ }
+ }
+
+ coutf("Generating %s size image for selector with scale %s\n", size, scale);
+ auto outputImage = make_shared<Image>(size, PixelFormat::RGB24);
+
+ for (size_t x = 0; x < size[0]; ++x) {
+ for (size_t y = 0; y < size[1]; ++y) {
+ // Image y = 0 is the top, so reverse it for the world position
+ auto value = terrainResult(x, y) / scale;
+ if (mode == "heatmap") {
+ Color color = Color::rgb(255, 0, 0);
+ color.setHue(clamp(value / 2 + 0.5f, 0.0f, 1.0f));
+ outputImage->set(x, y, color.toRgb());
+ } else if (mode == "terrain") {
+ if (value > 0)
+ outputImage->set(x, y, Vec3B(0, 100 + floor(155 * value), floor(255 * value)));
+ else
+ outputImage->set(x, y, Vec3B(floor(255 * -value), 0, 0));
+ }
+ }
+ }
+
+ outputImage->writePng(File::open("terrain.png", IOMode::Write));
+ return 0;
+ } catch (std::exception const& e) {
+ cerrf("exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/tileset_updater.cpp b/source/utility/tileset_updater.cpp
new file mode 100644
index 0000000..e94ac53
--- /dev/null
+++ b/source/utility/tileset_updater.cpp
@@ -0,0 +1,326 @@
+#include "StarLogging.hpp"
+#include "tileset_updater.hpp"
+
+using namespace Star;
+
+String const InvalidTileImage = "../packed/invalid.png";
+String const AssetsTilesetDirectory = "tilesets";
+String const TileImagesDirectory = "../../../../tiled";
+int const Indentation = 2;
+
+String unixFileJoin(String const& dirname, String const& filename) {
+ return (dirname.trimEnd("\\/") + '/' + filename.trimBeg("\\/")).replace("\\", "/");
+}
+
+TileDatabase::TileDatabase(String const& name) : m_name(name) {}
+
+void TileDatabase::defineTile(TilePtr const& tile) {
+ m_tiles[tile->name] = tile;
+}
+
+TilePtr TileDatabase::getTile(String const& tileName) const {
+ return m_tiles.maybe(tileName).value({});
+}
+
+String TileDatabase::name() const {
+ return m_name;
+}
+
+StringSet TileDatabase::tileNames() const {
+ return StringSet::from(m_tiles.keys());
+}
+
+Tileset::Tileset(String const& source, String const& name, TileDatabasePtr const& database)
+ : m_source(source), m_name(name), m_tiles(), m_database(database) {}
+
+void Tileset::defineTile(TilePtr const& tile) {
+ // Each tileset must be exported from a single database. When a tile switches
+ // to another tileset (e.g. because an object has changed category), we allow
+ // it to stay in the previous tileset to avoid breaking maps.
+ // This means that if we exported a mix of, e.g. materials, liquids and
+ // objects (which would cause the assertion failure below) it'd be harder to
+ // check if a tile still exists in the database and should be exported
+ // despite no longer belonging to the tileset.
+ starAssert(m_source == tile->source);
+ starAssert(m_database->name() == tile->database);
+
+ m_tiles.append(tile);
+}
+
+Maybe<pair<String, String>> parseAssetSource(String const& source) {
+ if (!File::isDirectory(source))
+ return {};
+
+ String sourcePath = source.trimEnd("/\\");
+ String sourceName = sourcePath.splitAny("/\\").last();
+
+ return make_pair(sourceName, sourcePath);
+}
+
+String tilesetExportDir(String const& sourcePath, String const& sourceName) {
+ return StringList{sourcePath, AssetsTilesetDirectory, sourceName}.join("/");
+}
+
+void Tileset::exportTileset() const {
+ auto parsedSource = parseAssetSource(m_source);
+ if (!parsedSource)
+ // Don't export tilesets into packed assets
+ return;
+
+ String sourceName, sourcePath;
+ tie(sourceName, sourcePath) = *parsedSource;
+ String exportDir = tilesetExportDir(sourcePath, sourceName);
+ String tilesetPath = unixFileJoin(exportDir, m_name + ".json");
+ File::makeDirectoryRecursive(File::dirName(tilesetPath));
+ Logger::info("Updating tileset at %s", tilesetPath);
+
+ exportTilesetImages(exportDir);
+
+ Json root = getTilesetJson(tilesetPath);
+ JsonObject tileImages = JsonObject{};
+ JsonObject tileProperties = root.getObject("tileproperties", JsonObject{});
+
+ // Scan the tiles already in the tileset
+ StringMap<size_t> existingTiles;
+ size_t nextId = 0;
+ tie(existingTiles, nextId) = indexExistingTiles(root);
+
+ // Add new tiles and update existing ones
+ StringSet updatedTiles = updateTiles(tileProperties, tileImages, existingTiles, nextId, tilesetPath);
+
+ // Mark all tiles that (a) already existed and (b) were not updated as invalid
+ // as they are no longer in the assets database.
+ StringSet invalidTiles = StringSet::from(existingTiles.keys()).difference(updatedTiles);
+ invalidateTiles(invalidTiles, existingTiles, tileProperties, tileImages, tilesetPath);
+
+ // We have some broken tile indices because of something strange happening
+ // in the old .tsx files (manual editing? faulty merges?).
+ // Cover up the holes so that Tiled doesn't barf on them.
+ for (size_t id = 0; id < nextId; ++id) {
+ String idKey = toString(id);
+ if (!tileProperties.contains(idKey))
+ tileProperties[idKey] = JsonObject{{"invalid", "true"}};
+ if (!tileImages.contains(idKey))
+ tileImages[idKey] = imageFileReference(InvalidTileImage);
+ }
+
+ root = root.set("tiles", tileImages).set("tileproperties", tileProperties);
+ root = root.set("tilecount", nextId);
+ File::writeFile(root.printJson(Indentation, true), tilesetPath);
+}
+
+String Tileset::name() const {
+ return m_name;
+}
+
+TileDatabasePtr Tileset::database() const {
+ return m_database;
+}
+
+String imageExportDirName(String const& baseExportDir, String const& assetSourceName) {
+ String dir = unixFileJoin(baseExportDir, TileImagesDirectory);
+ return unixFileJoin(dir, assetSourceName);
+}
+
+String Tileset::imageDirName(String const& baseExportDir) const {
+ String sourceName = parseAssetSource(m_source)->first;
+ return imageExportDirName(baseExportDir, sourceName);
+}
+
+String Tileset::relativePathBase() const {
+ int subdirs = m_name.splitAny("\\/").size() - 1;
+ String relativePathBase;
+ if (subdirs == 0) {
+ relativePathBase = ".";
+ } else {
+ StringList path;
+ for (int i = 0; i < subdirs; ++i)
+ path.append("..");
+ relativePathBase = path.join("/");
+ }
+ return relativePathBase;
+}
+
+Json Tileset::imageFileReference(String const& fileName) const {
+ String tileImagePath = unixFileJoin(imageDirName(relativePathBase()), fileName);
+ return JsonObject{{"image", tileImagePath}};
+}
+
+Json Tileset::tileImageReference(String const& tileName, String const& database) const {
+ String tileImageName = unixFileJoin(database, tileName + ".png");
+ return imageFileReference(tileImageName);
+}
+
+void Tileset::exportTilesetImages(String const& exportDir) const {
+ for (auto const& tile : m_tiles) {
+ String imageDir = unixFileJoin(imageDirName(exportDir), tile->database);
+ File::makeDirectoryRecursive(imageDir);
+ String imageName = unixFileJoin(imageDir, tile->name + ".png");
+ Logger::info("Updating image %s", imageName);
+ tile->image->writePng(File::open(imageName, IOMode::Write));
+ }
+}
+
+Json Tileset::getTilesetJson(String const& tilesetPath) const {
+ if (File::exists(tilesetPath)) {
+ return Json::parseJson(File::readFileString(tilesetPath));
+ } else {
+ Logger::warn(
+ "Tileset %s wasn't already present. Creating it from scratch. Any maps already using this tileset may be "
+ "broken.",
+ tilesetPath);
+ return JsonObject{{"margin", 0},
+ {"name", m_name},
+ {"properties", JsonObject{}},
+ {"spacing", 0},
+ {"tilecount", m_tiles.size()},
+ {"tileheight", TilePixels},
+ {"tilewidth", TilePixels},
+ {"tiles", JsonObject{}},
+ {"tileproperties", JsonObject{}}};
+ }
+}
+
+pair<StringMap<size_t>, size_t> Tileset::indexExistingTiles(Json tileset) const {
+ StringMap<size_t> existingTiles;
+ size_t nextId = 0;
+ for (auto const& entry : tileset.getObject("tileproperties")) {
+ size_t id = lexicalCast<size_t>(entry.first);
+ Tiled::Properties properties = entry.second;
+ if (properties.contains("//name")) {
+ existingTiles[properties.get<String>("//name")] = id;
+ nextId = max(id + 1, nextId);
+ }
+ }
+ return make_pair(existingTiles, nextId);
+}
+
+StringSet Tileset::updateTiles(JsonObject& tileProperties,
+ JsonObject& tileImages,
+ StringMap<size_t> const& existingTiles,
+ size_t& nextId,
+ String const& tilesetPath) const {
+ StringSet updatedTiles;
+ for (TilePtr const& tile : m_tiles) {
+ Tiled::Properties properties = tile->properties;
+ size_t id = 0;
+
+ if (existingTiles.contains(tile->name)) {
+ id = existingTiles.get(tile->name);
+ } else {
+ coutf("Adding '%s' to %s\n", tile->name, tilesetPath);
+ id = nextId++;
+ }
+
+ tileProperties[toString(id)] = properties.toJson();
+ tileImages[toString(id)] = tileImageReference(tile->name, tile->database);
+
+ updatedTiles.add(tile->name);
+ }
+ return updatedTiles;
+}
+
+void Tileset::invalidateTiles(StringSet const& invalidTiles,
+ StringMap<size_t> const& existingTiles,
+ JsonObject& tileProperties,
+ JsonObject& tileImages,
+ String const& tilesetPath) const {
+ for (String tileName : invalidTiles) {
+ size_t id = existingTiles.get(tileName);
+
+ if (TilePtr const& tile = m_database->getTile(tileName)) {
+ // Tile has moved category, but we're leaving it in this tileset to avoid
+ // breaking existing maps.
+ tileProperties[toString(id)] = tile->properties.toJson();
+ tileImages[toString(id)] = tileImageReference(tile->name, tile->database);
+ } else {
+ if (!tileProperties[toString(id)].contains("invalid"))
+ coutf("Removing '%s' from %s\n", tileName, tilesetPath);
+ tileProperties[toString(id)] = JsonObject{{"//name", tileName}, {"invalid", "true"}};
+ tileImages[toString(id)] = imageFileReference(InvalidTileImage);
+ }
+ }
+}
+
+void TilesetUpdater::defineAssetSource(String const& source) {
+ auto parsedSource = parseAssetSource(source);
+ if (!parsedSource)
+ // Don't change anything about images in packed assets
+ return;
+
+ String sourceName;
+ String sourcePath;
+ tie(sourceName, sourcePath) = *parsedSource;
+ String tilesetDir = tilesetExportDir(sourcePath, sourceName);
+ String imageDir = imageExportDirName(tilesetDir, sourceName);
+
+ Logger::info("Scanning %s for images...", imageDir);
+ if (!File::isDirectory(imageDir))
+ return;
+
+ for (pair<String, bool> entry : File::dirList(imageDir)) {
+ if (entry.second) {
+ String databaseName = entry.first;
+ String databasePath = unixFileJoin(imageDir, databaseName);
+ Logger::info("Scanning database %s...", databaseName);
+ for (pair<String, bool> image : File::dirList(databasePath)) {
+ starAssert(!image.second);
+ starAssert(image.first.endsWith(".png"));
+ String tileName = image.first.substr(0, image.first.findLast(".png"));
+ m_preexistingImages[sourceName][databaseName].add(tileName);
+ }
+ }
+ }
+}
+
+void TilesetUpdater::defineTile(TilePtr const& tile) {
+ getDatabase(tile)->defineTile(tile);
+ getTileset(tile)->defineTile(tile);
+}
+
+void TilesetUpdater::exportTilesets() {
+ for (auto const& tilesets : m_tilesets) {
+ auto parsedAssetSource = parseAssetSource(tilesets.first);
+ if (!parsedAssetSource) {
+ Logger::info("Not updating tilesets in %s because it is packed", tilesets.first);
+ continue;
+ }
+ String sourceName;
+ String sourcePath;
+ tie(sourceName, sourcePath) = *parsedAssetSource;
+
+ String tilesetDir = tilesetExportDir(sourcePath, sourceName);
+ String imageDir = imageExportDirName(tilesetDir, sourceName);
+
+ for (auto const& tileset : tilesets.second.values()) {
+ tileset->exportTileset();
+ }
+
+ for (auto const& database : m_databases[tilesets.first].values()) {
+ String databaseImagePath = unixFileJoin(imageDir, database->name());
+ StringSet unusedImages = m_preexistingImages[sourceName][database->name()].difference(database->tileNames());
+ for (String tileName : unusedImages) {
+ String tileImagePath = unixFileJoin(databaseImagePath, tileName + ".png");
+ starAssert(File::isFile(tileImagePath));
+ coutf("Removing unused tile image tiled/%s/%s/%s.png\n", sourceName, database->name(), tileName);
+ File::remove(tileImagePath);
+ }
+ m_preexistingImages[sourceName][database->name()] = database->tileNames();
+ }
+ }
+}
+
+TileDatabasePtr const& TilesetUpdater::getDatabase(TilePtr const& tile) {
+ auto& databases = m_databases[tile->source];
+ if (!databases.contains(tile->database))
+ databases[tile->database] = make_shared<TileDatabase>(tile->database);
+ return databases[tile->database];
+}
+
+TilesetPtr const& TilesetUpdater::getTileset(TilePtr const& tile) {
+ TileDatabasePtr database = getDatabase(tile);
+ auto& tilesets = m_tilesets[tile->source];
+ if (!tilesets.contains(tile->tileset))
+ tilesets[tile->tileset] = make_shared<Tileset>(tile->source, tile->tileset, database);
+ return tilesets[tile->tileset];
+}
diff --git a/source/utility/tileset_updater.hpp b/source/utility/tileset_updater.hpp
new file mode 100644
index 0000000..8848a00
--- /dev/null
+++ b/source/utility/tileset_updater.hpp
@@ -0,0 +1,102 @@
+#include "StarImage.hpp"
+#include "StarTilesetDatabase.hpp"
+
+namespace Star {
+
+STAR_STRUCT(Tile);
+STAR_CLASS(TileDatabase);
+STAR_CLASS(Tileset);
+
+struct Tile {
+ String source, database, tileset, name;
+ ImageConstPtr image;
+ Tiled::Properties properties;
+};
+
+class TileDatabase {
+public:
+ TileDatabase(String const& name);
+
+ void defineTile(TilePtr const& tile);
+ TilePtr getTile(String const& tileName) const;
+ String name() const;
+ StringSet tileNames() const;
+
+private:
+ Map<String, TilePtr> m_tiles;
+ String m_name;
+};
+
+class Tileset {
+public:
+ Tileset(String const& source, String const& name, TileDatabasePtr const& database);
+
+ void defineTile(TilePtr const& tile);
+ void exportTileset() const;
+
+ String name() const;
+ TileDatabasePtr database() const;
+
+private:
+ String imageDirName(String const& baseExportDir) const;
+ String relativePathBase() const;
+ Json imageFileReference(String const& fileName) const;
+ Json tileImageReference(String const& tileName, String const& database) const;
+
+ // Exports an image for each tile into its own file. Tiles can represent
+ // objects with all different sizes, so we use Tiled's "collection of images"
+ // tileset feature, which puts each image in its own file.
+ void exportTilesetImages(String const& exportDir) const;
+
+ // Read the tileset from the given path, or create a new tileset root
+ // structure if it doesn't already exist.
+ Json getTilesetJson(String const& tilesetPath) const;
+
+ // Determine which tiles already exist in the tileset, returning a map
+ // which contains the id of each named tile, and the next available Id after
+ // the highest Id seen in the tileset.
+ pair<StringMap<size_t>, size_t> indexExistingTiles(Json tileset) const;
+
+ // Update existing and insert new tile definitions in the tileProperties and
+ // tileImages objects.
+ StringSet updateTiles(JsonObject& tileProperties,
+ JsonObject& tileImages,
+ StringMap<size_t> const& existingTiles,
+ size_t& nextId,
+ String const& tilesetPath) const;
+
+ // Mark the given tiles as 'invalid' so they can't be used. (Actually removing
+ // them from the tileset would cause the tile indices to change and break
+ // existing maps.)
+ void invalidateTiles(StringSet const& invalidTiles,
+ StringMap<size_t> const& existingTiles,
+ JsonObject& tileProperties,
+ JsonObject& tileImages,
+ String const& tilesetPath) const;
+
+ String m_source, m_name;
+ List<TilePtr> m_tiles;
+ TileDatabasePtr m_database;
+};
+
+class TilesetUpdater {
+public:
+ void defineAssetSource(String const& source);
+ void defineTile(TilePtr const& tile);
+ void exportTilesets();
+
+private:
+ TileDatabasePtr const& getDatabase(TilePtr const& tile);
+ TilesetPtr const& getTileset(TilePtr const& tile);
+
+ // Asset Source -> Tileset Name -> Tileset
+ StringMap<StringMap<TilesetPtr>> m_tilesets;
+ // Asset Source -> Database Name -> Database
+ StringMap<StringMap<TileDatabasePtr>> m_databases;
+
+ // Images that existed before running update_tilesets:
+ // Asset Source -> Database Name -> Tile Name
+ StringMap<StringMap<StringSet>> m_preexistingImages;
+};
+
+}
diff --git a/source/utility/update_tilesets.cpp b/source/utility/update_tilesets.cpp
new file mode 100644
index 0000000..f21f886
--- /dev/null
+++ b/source/utility/update_tilesets.cpp
@@ -0,0 +1,262 @@
+#include "StarAssets.hpp"
+#include "StarLiquidsDatabase.hpp"
+#include "StarMaterialDatabase.hpp"
+#include "StarObject.hpp"
+#include "StarObjectDatabase.hpp"
+#include "StarRootLoader.hpp"
+#include "tileset_updater.hpp"
+
+using namespace Star;
+
+String const InboundNode = "/tilesets/inboundnode.png";
+String const OutboundNode = "/tilesets/outboundnode.png";
+Vec3B const SourceLiquidBorderColor(0x80, 0x80, 0x00);
+
+void scanMaterials(TilesetUpdater& updater) {
+ auto& root = Root::singleton();
+ auto materials = root.materialDatabase();
+
+ for (String materialName : materials->materialNames()) {
+ MaterialId id = materials->materialId(materialName);
+ Maybe<String> path = materials->materialPath(id);
+ if (!path)
+ continue;
+ String source = root.assets()->assetSource(*path);
+
+ auto renderProfile = materials->materialRenderProfile(id);
+ if (renderProfile == nullptr)
+ continue;
+
+ String tileset = materials->materialCategory(id);
+ String imagePath = renderProfile->pieceImage(renderProfile->representativePiece, 0);
+ ImageConstPtr image = root.assets()->image(imagePath);
+
+ Tiled::Properties properties;
+ properties.set("material", materialName);
+ properties.set("//name", materialName);
+ properties.set("//shortdescription", materials->materialShortDescription(id));
+ properties.set("//description", materials->materialDescription(id));
+
+ auto tile = make_shared<Tile>(Tile{source, "materials", tileset.toLower(), materialName, image, properties});
+ updater.defineTile(tile);
+ }
+}
+
+// imagePosition might not be aligned to a whole number, i.e. the image origin
+// might not align with the tile grid. We do, however want Tile Objects in Tiled
+// to be grid-aligned (valid positions are offset relative to the grid not
+// completely free-form), so we correct the alignment by adding padding to the
+// image that we export.
+// We're going to ignore the fact that some objects have imagePositions that
+// aren't even aligned _to pixels_ (e.g. giftsmallmonsterbox).
+Vec2U objectPositionPadding(Vec2I imagePosition) {
+ int pixelsX = imagePosition.x();
+ int pixelsY = imagePosition.y();
+
+ // Unsigned modulo operation gives the padding to use (in pixels)
+ unsigned padX = (unsigned)pixelsX % TilePixels;
+ unsigned padY = (unsigned)pixelsY % TilePixels;
+ return Vec2U(padX, padY);
+}
+
+StringSet categorizeObject(String const& objectName, Vec2U imageSize) {
+ if (imageSize[0] >= 256 || imageSize[1] >= 256)
+ return StringSet{"huge-objects"};
+
+ auto& root = Root::singleton();
+ auto assets = root.assets();
+ auto objects = root.objectDatabase();
+
+ Json defaultCategories = assets->json("/objects/defaultCategories.config");
+
+ auto objectConfig = objects->getConfig(objectName);
+
+ StringSet categories;
+ if (objectConfig->category != defaultCategories.getString("category"))
+ categories.insert("objects-by-category/" + objectConfig->category);
+ for (String const& tag : objectConfig->colonyTags)
+ categories.insert("objects-by-colonytag/" + tag);
+ if (objectConfig->type != defaultCategories.getString("objectType"))
+ categories.insert("objects-by-type/" + objectConfig->type);
+ if (objectConfig->race != defaultCategories.getString("race"))
+ categories.insert("objects-by-race/" + objectConfig->race);
+
+ if (categories.size() == 0)
+ categories.insert("objects-uncategorized");
+
+ return transform<StringSet>(categories, [](String const& category) { return category.toLower(); });
+}
+
+void drawNodes(ImagePtr const& image, Vec2I imagePosition, JsonArray nodes, String nodeImagePath) {
+ ImageConstPtr nodeImage = Root::singleton().assets()->image(nodeImagePath);
+ for (Json const& node : nodes) {
+ Vec2I nodePos = jsonToVec2I(node) * TilePixels + Vec2I(0, TilePixels - nodeImage->height());
+ Vec2U nodeImagePos = Vec2U(nodePos - imagePosition);
+ image->drawInto(nodeImagePos, *nodeImage);
+ }
+}
+
+void defineObjectOrientation(TilesetUpdater& updater,
+ String const& objectName,
+ List<ObjectOrientationPtr> const& orientations,
+ int orientationIndex) {
+ auto& root = Root::singleton();
+ auto assets = root.assets();
+ auto objects = root.objectDatabase();
+
+ ObjectOrientationPtr orientation = orientations[orientationIndex];
+
+ Vec2I imagePosition = Vec2I(orientation->imagePosition * TilePixels);
+
+ List<ImageConstPtr> layers;
+ unsigned width = 0, height = 0;
+ for (auto const& imageLayer : orientation->imageLayers) {
+ String imageName = imageLayer.imagePart().image.replaceTags(StringMap<String>{}, true, "default");
+
+ ImageConstPtr image = assets->image(imageName);
+ layers.append(image);
+ width = max(width, image->width());
+ height = max(height, image->height());
+ }
+
+ Vec2U imagePadding = objectPositionPadding(imagePosition);
+ imagePosition -= Vec2I(imagePadding);
+
+ // Padding is added to the right hand side as well as the left so that
+ // when objects are flipped in the editor, they're still aligned correctly.
+ Vec2U imageSize(width + 2 * imagePadding.x(), height + imagePadding.y());
+
+ ImagePtr combinedImage = make_shared<Image>(imageSize, PixelFormat::RGBA32);
+ combinedImage->fill(Vec4B(0, 0, 0, 0));
+ for (ImageConstPtr const& layer : layers) {
+ combinedImage->drawInto(imagePadding, *layer);
+ }
+
+ // Overlay the image with the wiring nodes:
+ auto objectConfig = objects->getConfig(objectName);
+
+ drawNodes(combinedImage, imagePosition, objectConfig->config.getArray("inputNodes", {}), InboundNode);
+ drawNodes(combinedImage, imagePosition, objectConfig->config.getArray("outputNodes", {}), OutboundNode);
+
+ ObjectPtr example = objects->createObject(objectName);
+
+ Tiled::Properties properties;
+ properties.set("object", objectName);
+ properties.set("imagePositionX", imagePosition.x());
+ properties.set("imagePositionY", imagePosition.y());
+ properties.set("//shortdescription", example->shortDescription());
+ properties.set("//description", example->description());
+
+ if (orientation->directionAffinity.isValid()) {
+ Direction direction = *orientation->directionAffinity;
+ if (orientation->flipImages)
+ direction = -direction;
+ properties.set("tilesetDirection", DirectionNames.getRight(direction));
+ }
+
+ StringSet tilesets = categorizeObject(objectName, imageSize);
+
+ // tileName becomes part of the filename for the tile's image. Different
+ // orientations require different images, so the tileName must be different
+ // for each orientation.
+ String tileName = objectName;
+ if (orientationIndex != 0)
+ tileName += "_orientation" + toString(orientationIndex);
+ properties.set("//name", tileName);
+
+ String source = assets->assetSource(objectConfig->path);
+
+ for (String const& tileset : tilesets) {
+ TilePtr tile = make_shared<Tile>(Tile{source, "objects", tileset, tileName, combinedImage, properties});
+ updater.defineTile(tile);
+ }
+}
+
+void scanObjects(TilesetUpdater& updater) {
+ auto& root = Root::singleton();
+ auto objects = root.objectDatabase();
+
+ for (String const& objectName : objects->allObjects()) {
+ auto orientations = objects->getOrientations(objectName);
+ if (orientations.size() < 1) {
+ Logger::warn("Object %s has no orientations and will not be exported", objectName);
+ continue;
+ }
+
+ // Always export the first orientation
+ ObjectOrientationPtr orientation = orientations[0];
+ defineObjectOrientation(updater, objectName, orientations, 0);
+
+ // If there are more than 2 orientations or the imagePositions are different
+ // then horizontal flipping in the editor is not enough to get all the
+ // orientations and display them correctly, so we export each orientation
+ // as a separate tile.
+ for (unsigned i = 1; i < orientations.size(); ++i) {
+ if (i >= 2 || orientation->imagePosition != orientations[i]->imagePosition)
+ defineObjectOrientation(updater, objectName, orientations, i);
+ }
+ }
+}
+
+void scanLiquids(TilesetUpdater& updater) {
+ auto& root = Root::singleton();
+ auto liquids = root.liquidsDatabase();
+ auto assets = root.assets();
+
+ Vec2U imageSize(TilePixels, TilePixels);
+
+ for (auto liquid : liquids->allLiquidSettings()) {
+ ImagePtr image = make_shared<Image>(imageSize, PixelFormat::RGBA32);
+ image->fill(liquid->liquidColor);
+
+ String assetSource = assets->assetSource(liquid->path);
+
+ Tiled::Properties properties;
+ properties.set("liquid", liquid->name);
+ properties.set("//name", liquid->name);
+ auto tile = make_shared<Tile>(Tile{assetSource, "liquids", "liquids", liquid->name, image, properties});
+ updater.defineTile(tile);
+
+ ImagePtr sourceImage = make_shared<Image>(imageSize, PixelFormat::RGBA32);
+ sourceImage->copyInto(Vec2U(), *image.get());
+ sourceImage->fillRect(Vec2U(), Vec2U(image->width(), 1), SourceLiquidBorderColor);
+ sourceImage->fillRect(Vec2U(), Vec2U(1, image->height()), SourceLiquidBorderColor);
+
+ String sourceName = liquid->name + "_source";
+ properties.set("source", true);
+ properties.set("//name", sourceName);
+ properties.set("//shortdescription", "Endless " + liquid->name);
+ auto sourceTile = make_shared<Tile>(Tile{assetSource, "liquids", "liquids", sourceName, sourceImage, properties});
+ updater.defineTile(sourceTile);
+ }
+}
+
+int main(int argc, char** argv) {
+ try {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}});
+
+ rootLoader.setSummary("Updates Tiled JSON tilesets in unpacked assets directories");
+
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ TilesetUpdater updater;
+
+ for (String source : root->assets()->assetSources()) {
+ Logger::info("Assets source: \"%s\"", source);
+ updater.defineAssetSource(source);
+ }
+
+ scanMaterials(updater);
+ scanObjects(updater);
+ scanLiquids(updater);
+
+ updater.exportTilesets();
+
+ return 0;
+ } catch (std::exception const& e) {
+ cerrf("exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/word_count.cpp b/source/utility/word_count.cpp
new file mode 100644
index 0000000..683a3a9
--- /dev/null
+++ b/source/utility/word_count.cpp
@@ -0,0 +1,191 @@
+#include "StarFile.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarImage.hpp"
+#include "StarRootLoader.hpp"
+#include "StarAssets.hpp"
+#include "StarItemDatabase.hpp"
+#include "StarJson.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}});
+
+ rootLoader.setSummary("Calculate a (very approximate) word count of user-facing text in assets");
+
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ StringMap<int> wordCounts;
+ auto assets = Root::singleton().assets();
+
+ auto countWordsInType = [&](String const& type, function<int(Json const&)> countFunction, Maybe<function<bool(String const&)>> filterFunction = {}, Maybe<String> wordCountKey = {}) {
+ auto files = assets->scanExtension(type);
+ if (filterFunction)
+ files.filter(*filterFunction);
+ assets->queueJsons(files);
+ for (auto path : files) {
+ auto json = assets->json(path);
+ if (json.isNull())
+ continue;
+
+ String countKey = wordCountKey ? *wordCountKey : strf(".%s files", type);
+ wordCounts[countKey] += countFunction(json);
+ }
+ };
+
+ StringList itemFileTypes = {
+ "tech",
+ "item",
+ "liqitem",
+ "matitem",
+ "miningtool",
+ "flashlight",
+ "wiretool",
+ "beamaxe",
+ "tillingtool",
+ "painttool",
+ "harvestingtool",
+ "head",
+ "chest",
+ "legs",
+ "back",
+ "currencyitem",
+ "consumable",
+ "blueprint",
+ "inspectiontool",
+ "instrument",
+ "thrownitem",
+ "unlock",
+ "activeitem",
+ "augment" };
+
+ for (auto itemFileType : itemFileTypes) {
+ countWordsInType(itemFileType, [](Json const& json) {
+ int wordCount = 0;
+ wordCount += json.getString("shortdescription", "").split(" ").count();
+ wordCount += json.getString("description", "").split(" ").count();
+ return wordCount;
+ });
+ }
+
+ countWordsInType("object", [](Json const& json) {
+ int wordCount = 0;
+ wordCount += json.getString("shortdescription", "").split(" ").count();
+ wordCount += json.getString("description", "").split(" ").count();
+ wordCount += json.getString("apexDescription", "").split(" ").count();
+ wordCount += json.getString("avianDescription", "").split(" ").count();
+ wordCount += json.getString("glitchDescription", "").split(" ").count();
+ wordCount += json.getString("floranDescription", "").split(" ").count();
+ wordCount += json.getString("humanDescription", "").split(" ").count();
+ wordCount += json.getString("hylotlDescription", "").split(" ").count();
+ wordCount += json.getString("novakidDescription", "").split(" ").count();
+ return wordCount;
+ });
+
+ countWordsInType("codex", [](Json const& json) {
+ int wordCount = 0;
+ wordCount += json.getString("title", "").split(" ").count();
+ wordCount += json.getString("description", "").split(" ").count();
+ for (auto contentPage : json.getArray("contentPages", JsonArray()))
+ wordCount += contentPage.toString().split(" ").count();
+ return wordCount;
+ });
+
+ countWordsInType("monstertype", [](Json const& json) {
+ return json.getString("description", "").split(" ").count();
+ });
+
+ countWordsInType("radiomessages", [](Json const& json) {
+ auto wordCount = 0;
+ for (auto messageConfigPair : json.iterateObject())
+ wordCount += messageConfigPair.second.getString("text", "").split(" ").count();
+ return wordCount;
+ });
+
+ function<int(Json const& json)> countOnlyStrings;
+ countOnlyStrings = [&](Json const& json) {
+ int wordCount = 0;
+ if (json.isType(Json::Type::Object)) {
+ for (auto entry : json.iterateObject())
+ wordCount += countOnlyStrings(entry.second);
+ } else if (json.isType(Json::Type::Array)) {
+ for (auto entry : json.iterateArray())
+ wordCount += countOnlyStrings(entry);
+ } else if (json.isType(Json::Type::String)) {
+ if (!json.toString().beginsWith("/")) {
+ wordCount += json.toString().split(" ").count();
+ }
+ }
+ return wordCount;
+ };
+
+ function<bool(String const&)> dialogFilter = [](String const& filePath) { return filePath.beginsWith("/dialog/"); };
+ countWordsInType("config", countOnlyStrings, dialogFilter, String("NPC dialog (.config files)"));
+
+ countWordsInType("npctype", [&](Json const& json) {
+ if (auto scriptConfig = json.get("scriptConfig", Json()))
+ return countOnlyStrings(scriptConfig.get("dialog", Json()));
+ return 0;
+ }, {}, String("NPC dialog (.npctype files)"));
+
+ countWordsInType("questtemplate", [&](Json const& json) {
+ int wordCount = 0;
+ wordCount += json.getString("title", "").split(" ").count();
+ wordCount += json.getString("text", "").split(" ").count();
+ wordCount += json.getString("completionText", "").split(" ").count();
+ if (auto scriptConfig = json.get("scriptConfig", Json()))
+ wordCount += countOnlyStrings(scriptConfig.get("generatedText", Json()));
+ return wordCount;
+ });
+
+ countWordsInType("collection", [&](Json const& json) {
+ int wordCount = 0;
+ for (auto entry : json.get("collectables", Json()).iterateObject())
+ wordCount += entry.second.getString("description", "").split(" ").count();
+ return wordCount;
+ });
+
+ countWordsInType("cinematic", [&](Json const& json) {
+ int wordCount = 0;
+ for (auto panel : json.get("panels", Json()).iterateArray()) {
+ auto panelText = panel.optString("text");
+ // filter on pipes to ignore those long lists of backer names in the credits
+ if (panelText && !panelText->contains("|"))
+ wordCount += panelText->split(" ").count();
+ }
+ return wordCount;
+ });
+
+ countWordsInType("aimission", [&](Json const& json) {
+ int wordCount = 0;
+ for (auto entry : json.get("speciesText", Json()).iterateObject()) {
+ wordCount += entry.second.getString("buttonText", "").split(" ").count();
+ wordCount += entry.second.getString("repeatButtonText", "").split(" ").count();
+ if (auto selectSpeech = entry.second.get("selectSpeech"))
+ wordCount += selectSpeech.getString("text", "").split(" ").count();
+ }
+ return wordCount;
+ });
+
+ auto cockpitConfig = assets->json("/interface/cockpit/cockpit.config");
+ int cockpitWordCount = 0;
+ cockpitWordCount += countOnlyStrings(cockpitConfig.get("visitableTypeDescription"));
+ cockpitWordCount += countOnlyStrings(cockpitConfig.get("worldTypeDescription"));
+ wordCounts["planet descriptions (cockpit.config)"] = cockpitWordCount;
+
+ int totalWordCount = 0;
+ for (auto countPair : wordCounts) {
+ coutf("%d words in %s\n", countPair.second, countPair.first);
+ totalWordCount += countPair.second;
+ }
+ coutf("approximately %s words total\n", totalWordCount);
+
+ return 0;
+ } catch (std::exception const& e) {
+ cerrf("exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/utility/world_benchmark.cpp b/source/utility/world_benchmark.cpp
new file mode 100644
index 0000000..d9efe45
--- /dev/null
+++ b/source/utility/world_benchmark.cpp
@@ -0,0 +1,101 @@
+#include "StarLexicalCast.hpp"
+#include "StarLogging.hpp"
+#include "StarRootLoader.hpp"
+#include "StarWorldServer.hpp"
+#include "StarWorldTemplate.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ try {
+ RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}});
+ rootLoader.addArgument("dungeon", OptionParser::Required, "name of the dungeon to spawn in the world to benchmark");
+ rootLoader.addParameter("seed", "seed", OptionParser::Optional, "world seed used to create the WorldTemplate");
+ rootLoader.addParameter("steps", "steps", OptionParser::Optional, "number of steps to run the world for, defaults to 5,000");
+ rootLoader.addParameter("times", "times", OptionParser::Optional, "how many times to perform the run, defaults to once");
+ rootLoader.addParameter("signalevery", "signal steps", OptionParser::Optional, "number of steps to wait between scanning and signaling all entities to stay alive, default 120");
+ rootLoader.addParameter("reportevery", "report steps", OptionParser::Optional, "number of steps between each progress report, default 0 (do not report progress)");
+ rootLoader.addParameter("fidelity", "server fidelity", OptionParser::Optional, "fidelity to run the server with, default high");
+ rootLoader.addSwitch("profiling", "whether to use lua profiling, prints the profile with info logging");
+ rootLoader.addSwitch("unsafe", "enables unsafe lua libraries");
+ RootUPtr root;
+ OptionParser::Options options;
+ tie(root, options) = rootLoader.commandInitOrDie(argc, argv);
+
+ coutf("Fully loading root...");
+ root->fullyLoad();
+ coutf(" done\n");
+
+ String dungeon = options.arguments.first();
+ VisitableWorldParametersPtr worldParameters = generateFloatingDungeonWorldParameters(dungeon);
+ uint64_t worldSeed = Random::randu64();
+ if (options.parameters.contains("seed"))
+ worldSeed = lexicalCast<uint64_t>(options.parameters.get("seed").first());
+ auto worldTemplate = make_shared<WorldTemplate>(worldParameters, SkyParameters(), worldSeed);
+
+ auto fidelity = options.parameters.maybe("fidelity").apply([](StringList p) { return p.maybeFirst(); }).value({});
+ root->configuration()->set("serverFidelity", fidelity.value("high"));
+
+ if (options.switches.contains("unsafe"))
+ root->configuration()->set("safeScripts", false);
+ if (options.switches.contains("profiling")) {
+ root->configuration()->set("scriptProfilingEnabled", true);
+ root->configuration()->set("scriptInstructionMeasureInterval", 100);
+ }
+
+ uint64_t times = 1;
+ if (options.parameters.contains("times"))
+ times = lexicalCast<uint64_t>(options.parameters.get("times").first());
+
+ uint64_t steps = 5000;
+ if (options.parameters.contains("steps"))
+ steps = lexicalCast<uint64_t>(options.parameters.get("steps").first());
+
+ uint64_t signalEvery = 120;
+ if (options.parameters.contains("signalevery"))
+ signalEvery = lexicalCast<uint64_t>(options.parameters.get("signalevery").first());
+
+ uint64_t reportEvery = 0;
+ if (options.parameters.contains("reportevery"))
+ reportEvery = lexicalCast<uint64_t>(options.parameters.get("reportevery").first());
+
+ double sumTime = 0.0;
+ for (uint64_t i = 0; i < times; ++i) {
+ WorldServer worldServer(worldTemplate, File::ephemeralFile());
+
+ coutf("Starting world simulation for %s steps\n", steps);
+ double start = Time::monotonicTime();
+ double lastReport = Time::monotonicTime();
+ uint64_t entityCount = 0;
+ for (uint64_t j = 0; j < steps; ++j) {
+ if (j % signalEvery == 0) {
+ entityCount = 0;
+ worldServer.forEachEntity(RectF(Vec2F(), Vec2F(worldServer.geometry().size())), [&](auto const& entity) {
+ ++entityCount;
+ worldServer.signalRegion(RectI::integral(entity->metaBoundBox().translated(entity->position())));
+ });
+ }
+
+ if (reportEvery != 0 && j % reportEvery == 0) {
+ float fps = reportEvery / (Time::monotonicTime() - lastReport);
+ lastReport = Time::monotonicTime();
+ coutf("[%s] %ss | FPS: %s | Entities: %s\n", j, Time::monotonicTime() - start, fps, entityCount);
+ }
+ worldServer.update();
+ }
+ double totalTime = Time::monotonicTime() - start;
+ coutf("Finished run of running dungeon world '%s' with seed %s for %s steps in %s seconds, average FPS: %s\n",
+ dungeon, worldSeed, steps, totalTime, steps / totalTime);
+ sumTime += totalTime;
+ }
+
+ if (times != 1) {
+ coutf("Average of all runs - time: %s, FPS: %s\n", sumTime / times, steps / (sumTime / times));
+ }
+
+ return 0;
+ } catch (std::exception const& e) {
+ cerrf("Exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}