v1.4.4
This commit is contained in:
commit
9c94d113d3
10260 changed files with 1237388 additions and 0 deletions
77
source/utility/CMakeLists.txt
Normal file
77
source/utility/CMakeLists.txt
Normal 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})
|
79
source/utility/asset_packer.cpp
Normal file
79
source/utility/asset_packer.cpp
Normal 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;
|
||||
}
|
||||
}
|
53
source/utility/asset_unpacker.cpp
Normal file
53
source/utility/asset_unpacker.cpp
Normal 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;
|
||||
}
|
||||
}
|
20
source/utility/dump_versioned_json.cpp
Normal file
20
source/utility/dump_versioned_json.cpp
Normal 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;
|
||||
}
|
||||
}
|
61
source/utility/dungeon_generation_benchmark.cpp
Normal file
61
source/utility/dungeon_generation_benchmark.cpp
Normal 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;
|
||||
}
|
||||
}
|
101
source/utility/fix_embedded_tilesets.cpp
Normal file
101
source/utility/fix_embedded_tilesets.cpp
Normal 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;
|
||||
}
|
||||
}
|
53
source/utility/game_repl.cpp
Normal file
53
source/utility/game_repl.cpp
Normal 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;
|
||||
}
|
81
source/utility/generation_benchmark.cpp
Normal file
81
source/utility/generation_benchmark.cpp
Normal 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;
|
||||
}
|
||||
}
|
20
source/utility/make_versioned_json.cpp
Normal file
20
source/utility/make_versioned_json.cpp
Normal 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
116
source/utility/map_grep.cpp
Normal 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;
|
||||
}
|
||||
}
|
117
source/utility/planet_mapgen.cpp
Normal file
117
source/utility/planet_mapgen.cpp
Normal 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;
|
||||
}
|
||||
}
|
96
source/utility/render_terrain_selector.cpp
Normal file
96
source/utility/render_terrain_selector.cpp
Normal 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;
|
||||
}
|
||||
}
|
326
source/utility/tileset_updater.cpp
Normal file
326
source/utility/tileset_updater.cpp
Normal 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];
|
||||
}
|
102
source/utility/tileset_updater.hpp
Normal file
102
source/utility/tileset_updater.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
262
source/utility/update_tilesets.cpp
Normal file
262
source/utility/update_tilesets.cpp
Normal 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;
|
||||
}
|
||||
}
|
191
source/utility/word_count.cpp
Normal file
191
source/utility/word_count.cpp
Normal 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;
|
||||
}
|
||||
}
|
101
source/utility/world_benchmark.cpp
Normal file
101
source/utility/world_benchmark.cpp
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue