This commit is contained in:
Aria 2025-03-21 22:23:30 +11:00
commit 9c94d113d3
Signed by untrusted user who does not match committer: aria
GPG key ID: 19AB7AA462B8AB3B
10260 changed files with 1237388 additions and 0 deletions

View file

@ -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})

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

116
source/utility/map_grep.cpp Normal file
View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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];
}

View file

@ -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;
};
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}