Files
EVEngine/src/modules/pixelworld/PixelWorldGenerationCodec.cpp
T

205 lines
8.7 KiB
C++

#include "pixelworld/PixelWorldGenerationCodec.h"
#include <Poco/Dynamic/Var.h>
#include <Poco/JSON/Array.h>
#include <Poco/JSON/Object.h>
#include <Poco/JSON/Parser.h>
#include <Poco/JSON/Stringifier.h>
#include <charconv>
#include <limits>
#include <sstream>
#include <type_traits>
#include <unordered_set>
namespace eve::pixelworld {
namespace {
using Object = Poco::JSON::Object;
using Array = Poco::JSON::Array;
eve::Result<PixelWorldGenerationRequest> malformed(std::string message,
std::string path = "document") {
return eve::Result<PixelWorldGenerationRequest>::failure(eve::Diagnostic::error(
eve::DiagnosticCode::SerializationError, std::move(message), std::move(path), {},
"pixelworld.generation-codec"));
}
bool exactFields(const Object& object, std::initializer_list<std::string_view> allowed) {
std::unordered_set<std::string> names;
for (const auto name : allowed) names.emplace(name);
if (object.size() != names.size()) return false;
for (const auto& name : object.getNames())
if (!names.contains(name)) return false;
for (const auto& name : names)
if (!object.has(name)) return false;
return true;
}
template <class T>
bool readNumber(const Object& object, const std::string& key, T& result) {
static_assert(std::is_integral_v<T> && !std::is_same_v<T, bool>);
if (!object.has(key) || object.isNull(key)) return false;
try {
const auto wide = object.get(key).convert<std::int64_t>();
if constexpr (std::is_unsigned_v<T>) {
if (wide < 0 || std::uint64_t(wide) > std::numeric_limits<T>::max()) return false;
} else if (wide < std::numeric_limits<T>::min() ||
wide > std::numeric_limits<T>::max()) {
return false;
}
result = static_cast<T>(wide);
return true;
} catch (...) {
return false;
}
}
bool readSeed(const Object& object, std::uint64_t& seed) {
if (!object.has("seed") || object.isNull("seed")) return false;
try {
const std::string text = object.getValue<std::string>("seed");
const char* begin = text.data();
const char* end = begin + text.size();
const auto parsed = std::from_chars(begin, end, seed);
return parsed.ec == std::errc{} && parsed.ptr == end;
} catch (...) {
return false;
}
}
Object::Ptr cellJson(const PixelCell& cell) {
Object::Ptr out(new Object);
out->set("material", std::uint16_t(cell.material));
out->set("temperature", cell.temperature);
out->set("lifetime", cell.lifetime);
out->set("thermalRemainder", cell.thermalRemainder);
return out;
}
} // namespace
eve::Result<std::string> encodePixelWorldGenerationRequestJson(
const PixelWorldGenerationRequest& request) {
Object::Ptr root(new Object);
root->set("schema", "eve.pixelworld.generation-request");
root->set("version", request.schemaVersion);
root->set("seed", std::to_string(request.seed));
Object::Ptr region(new Object);
region->set("minX", request.region.minX);
region->set("minY", request.region.minY);
region->set("maxX", request.region.maxX);
region->set("maxY", request.region.maxY);
root->set("region", region);
root->set("surfaceY", request.surfaceY);
root->set("terrainAmplitude", request.terrainAmplitude);
root->set("waterLevel", request.waterLevel);
root->set("caveThreshold", request.caveThreshold);
Array::Ptr stamps(new Array);
for (const PixelMaterialStamp& stamp : request.stamps) {
Object::Ptr value(new Object);
value->set("originX", stamp.originX);
value->set("originY", stamp.originY);
value->set("width", stamp.width);
value->set("height", stamp.height);
Array::Ptr cells(new Array);
for (const PixelCell& cell : stamp.cells) cells->add(cellJson(cell));
value->set("cells", cells);
stamps->add(value);
}
root->set("stamps", stamps);
std::ostringstream text;
Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(root), text, 2, -1);
return eve::Result<std::string>::success(text.str());
}
eve::Result<PixelWorldGenerationRequest> decodePixelWorldGenerationRequestJson(
std::string_view json) {
Object::Ptr root;
try {
root = Poco::JSON::Parser().parse(std::string(json)).extract<Object::Ptr>();
} catch (...) {
return malformed("generation request JSON is malformed");
}
if (!root || !exactFields(*root, {"schema", "version", "seed", "region", "surfaceY",
"terrainAmplitude", "waterLevel", "caveThreshold", "stamps"}))
return malformed("root contains missing or unknown fields");
if (root->optValue<std::string>("schema", "") != "eve.pixelworld.generation-request")
return malformed("unknown generation request schema", "schema");
PixelWorldGenerationRequest request;
if (!readNumber(*root, "version", request.schemaVersion) ||
request.schemaVersion != PixelWorldGenerationRequest::kSchemaVersion)
return malformed("unknown generation request version", "version");
if (!readSeed(*root, request.seed)) return malformed("seed is not a canonical uint64", "seed");
Object::Ptr region;
Array::Ptr stamps;
try {
region = root->getObject("region");
stamps = root->getArray("stamps");
} catch (...) {
return malformed("region or stamps has an invalid type");
}
if (!region || !exactFields(*region, {"minX", "minY", "maxX", "maxY"}) || !stamps ||
stamps->size() > 65'536U)
return malformed("region fields or stamp budget are invalid");
if (!readNumber(*region, "minX", request.region.minX) ||
!readNumber(*region, "minY", request.region.minY) ||
!readNumber(*region, "maxX", request.region.maxX) ||
!readNumber(*region, "maxY", request.region.maxY) ||
!readNumber(*root, "surfaceY", request.surfaceY) ||
!readNumber(*root, "terrainAmplitude", request.terrainAmplitude) ||
!readNumber(*root, "waterLevel", request.waterLevel) ||
!readNumber(*root, "caveThreshold", request.caveThreshold))
return malformed("generation request contains an invalid number");
std::uint64_t totalCells = 0;
request.stamps.reserve(stamps->size());
for (std::size_t index = 0; index < stamps->size(); ++index) {
Object::Ptr value;
Array::Ptr cells;
try {
value = stamps->getObject(unsigned(index));
if (value) cells = value->getArray("cells");
} catch (...) {
return malformed("stamp is not an object", "stamps[" + std::to_string(index) + "]");
}
if (!value || !exactFields(*value, {"originX", "originY", "width", "height", "cells"}) ||
!cells)
return malformed("stamp contains missing or unknown fields",
"stamps[" + std::to_string(index) + "]");
PixelMaterialStamp stamp;
if (!readNumber(*value, "originX", stamp.originX) ||
!readNumber(*value, "originY", stamp.originY) ||
!readNumber(*value, "width", stamp.width) ||
!readNumber(*value, "height", stamp.height) || stamp.width <= 0 || stamp.height <= 0 ||
std::uint64_t(stamp.width) * std::uint64_t(stamp.height) != cells->size())
return malformed("stamp dimensions are invalid", "stamps[" + std::to_string(index) + "]");
totalCells += cells->size();
if (totalCells > 4'194'304ULL) return malformed("stamp cell budget is exceeded", "stamps");
stamp.cells.reserve(cells->size());
for (std::size_t cellIndex = 0; cellIndex < cells->size(); ++cellIndex) {
Object::Ptr item;
try {
item = cells->getObject(unsigned(cellIndex));
} catch (...) {
return malformed("stamp cell is not an object", "stamps.cells");
}
PixelCell cell;
std::uint16_t material = 0;
if (!item || !exactFields(*item, {"material", "temperature", "lifetime", "thermalRemainder"}) ||
!readNumber(*item, "material", material) ||
!readNumber(*item, "temperature", cell.temperature) ||
!readNumber(*item, "lifetime", cell.lifetime) ||
!readNumber(*item, "thermalRemainder", cell.thermalRemainder))
return malformed("stamp cell contains invalid fields", "stamps.cells");
cell.material = MaterialId(material);
stamp.cells.push_back(cell);
}
request.stamps.push_back(std::move(stamp));
}
return eve::Result<PixelWorldGenerationRequest>::success(std::move(request));
}
} // namespace eve::pixelworld