diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/utility | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/utility')
-rw-r--r-- | source/utility/CMakeLists.txt | 77 | ||||
-rw-r--r-- | source/utility/asset_packer.cpp | 79 | ||||
-rw-r--r-- | source/utility/asset_unpacker.cpp | 53 | ||||
-rw-r--r-- | source/utility/dump_versioned_json.cpp | 20 | ||||
-rw-r--r-- | source/utility/dungeon_generation_benchmark.cpp | 61 | ||||
-rw-r--r-- | source/utility/fix_embedded_tilesets.cpp | 101 | ||||
-rw-r--r-- | source/utility/game_repl.cpp | 53 | ||||
-rw-r--r-- | source/utility/generation_benchmark.cpp | 81 | ||||
-rw-r--r-- | source/utility/make_versioned_json.cpp | 20 | ||||
-rw-r--r-- | source/utility/map_grep.cpp | 116 | ||||
-rw-r--r-- | source/utility/planet_mapgen.cpp | 117 | ||||
-rw-r--r-- | source/utility/render_terrain_selector.cpp | 96 | ||||
-rw-r--r-- | source/utility/tileset_updater.cpp | 326 | ||||
-rw-r--r-- | source/utility/tileset_updater.hpp | 102 | ||||
-rw-r--r-- | source/utility/update_tilesets.cpp | 262 | ||||
-rw-r--r-- | source/utility/word_count.cpp | 191 | ||||
-rw-r--r-- | source/utility/world_benchmark.cpp | 101 |
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; + } +} |