Grids can be saved in files and used online
This commit is contained in:
parent
0c0d7d8439
commit
52de64907a
14 changed files with 122 additions and 42 deletions
|
|
@ -22,9 +22,16 @@ class BookmarkManager {
|
|||
void remove_bookmark(const std::string& name);
|
||||
void jump_to_bookmark(const std::string& name);
|
||||
const std::unordered_map<std::string, Bookmark>& bookmark_list();
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
|
||||
template <typename Archive> void save(Archive& a) const {
|
||||
a(bookmarks);
|
||||
}
|
||||
|
||||
template <typename Archive> void load(Archive& a) {
|
||||
a(bookmarks);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const std::vector<std::string>& sorted_names();
|
||||
private:
|
||||
bool changed = true;
|
||||
|
|
|
|||
|
|
@ -233,9 +233,9 @@ void DrawingProgram::tool_options_gui() {
|
|||
}
|
||||
|
||||
void DrawingProgram::modify_grid(ServerClientID gridToModifyID) {
|
||||
switch_to_tool(DrawingProgramToolType::GRIDMODIFY);
|
||||
GridModifyTool* gridModToolPtr = static_cast<GridModifyTool*>(drawTool.get());
|
||||
gridModToolPtr->set_grid_id(gridToModifyID);
|
||||
std::unique_ptr<GridModifyTool> newTool(std::make_unique<GridModifyTool>(*this));
|
||||
newTool->set_grid_id(gridToModifyID);
|
||||
switch_to_tool_ptr(std::move(newTool));
|
||||
}
|
||||
|
||||
void DrawingProgram::update() {
|
||||
|
|
|
|||
|
|
@ -171,4 +171,5 @@ void GridModifyTool::draw(SkCanvas* canvas, const DrawData& drawData) {
|
|||
}
|
||||
|
||||
void GridModifyTool::switch_tool(DrawingProgramToolType newTool) {
|
||||
drawP.world.gridMan.send_grid_info(gridID);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "GridManager.hpp"
|
||||
#include "Server/CommandList.hpp"
|
||||
#include "SharedTypes.hpp"
|
||||
#include "World.hpp"
|
||||
#include "MainProgram.hpp"
|
||||
|
|
@ -8,6 +9,19 @@ GridManager::GridManager(World& w):
|
|||
world(w) {}
|
||||
|
||||
void GridManager::init_client_callbacks() {
|
||||
world.con.client_add_recv_callback(CLIENT_SET_GRID, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
ServerClientID gID;
|
||||
message(gID);
|
||||
auto& g = grids[gID];
|
||||
message(g);
|
||||
changed = true;
|
||||
});
|
||||
world.con.client_add_recv_callback(CLIENT_REMOVE_GRID, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
ServerClientID gID;
|
||||
message(gID);
|
||||
grids.erase(gID);
|
||||
changed = true;
|
||||
});
|
||||
}
|
||||
|
||||
ServerClientID GridManager::add_default_grid(const std::string& newName) {
|
||||
|
|
@ -22,8 +36,17 @@ ServerClientID GridManager::add_default_grid(const std::string& newName) {
|
|||
return newID;
|
||||
}
|
||||
|
||||
void GridManager::send_grid_info(ServerClientID gridID) {
|
||||
auto gridFoundIt = grids.find(gridID);
|
||||
if(gridFoundIt != grids.end()) {
|
||||
WorldGrid& g = gridFoundIt->second;
|
||||
world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_SET_GRID, gridID, g);
|
||||
}
|
||||
}
|
||||
|
||||
void GridManager::remove_grid(ServerClientID idToRemove) {
|
||||
grids.erase(idToRemove);
|
||||
world.con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_REMOVE_GRID, idToRemove);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +66,15 @@ const std::vector<ServerClientID>& GridManager::sorted_grid_ids() {
|
|||
return sortedGridIDs;
|
||||
}
|
||||
|
||||
ClientPortionID GridManager::get_max_id(ServerPortionID serverID) const {
|
||||
ClientPortionID maxClientID = 0;
|
||||
for(auto& p : grids) {
|
||||
if(p.first.first == serverID)
|
||||
maxClientID = std::max(maxClientID, p.first.second);
|
||||
}
|
||||
return maxClientID;
|
||||
}
|
||||
|
||||
void GridManager::draw(SkCanvas* canvas, const DrawData& drawData) {
|
||||
for(auto& [str, g] : grids)
|
||||
g.draw(*this, canvas, drawData);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,16 @@ class GridManager {
|
|||
|
||||
ServerClientID add_default_grid(const std::string& newName);
|
||||
|
||||
template <typename Archive> void save(Archive& a) const {
|
||||
a(grids);
|
||||
}
|
||||
|
||||
template <typename Archive> void load(Archive& a) {
|
||||
a(grids);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
void send_grid_info(ServerClientID gridID);
|
||||
void remove_grid(ServerClientID idToRemove);
|
||||
void draw(SkCanvas* canvas, const DrawData& drawData);
|
||||
ClientPortionID get_max_id(ServerPortionID serverID) const;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ClientData {
|
|||
|
||||
Vector3f cursorColor;
|
||||
std::string displayName;
|
||||
ServerClientID id = {0, 0};
|
||||
ServerPortionID serverID = 0;
|
||||
|
||||
std::queue<ResourceData> pendingResourceData;
|
||||
std::queue<ServerClientID> pendingResourceID;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ enum ServerCommands : MessageCommandType {
|
|||
SERVER_CHAT_MESSAGE,
|
||||
SERVER_NEW_BOOKMARK,
|
||||
SERVER_REMOVE_BOOKMARK,
|
||||
SERVER_CANVAS_COLOR
|
||||
SERVER_CANVAS_COLOR,
|
||||
SERVER_SET_GRID,
|
||||
SERVER_REMOVE_GRID
|
||||
};
|
||||
|
||||
enum ClientCommands : MessageCommandType {
|
||||
|
|
@ -38,11 +40,14 @@ enum ClientCommands : MessageCommandType {
|
|||
CLIENT_CHAT_MESSAGE,
|
||||
CLIENT_NEW_BOOKMARK,
|
||||
CLIENT_REMOVE_BOOKMARK,
|
||||
CLIENT_CANVAS_COLOR
|
||||
CLIENT_CANVAS_COLOR,
|
||||
CLIENT_SET_GRID,
|
||||
CLIENT_REMOVE_GRID
|
||||
};
|
||||
|
||||
// NOTE: Do not change savefile header length
|
||||
#define SAVEFILE_HEADER_LEN 12
|
||||
// NOTE: Change these values whenever savefile is incompatible with previous version
|
||||
#define SAVEFILE_HEADER "INFPNT000002"
|
||||
#define SAVEFILE_HEADER "INFPNT000003"
|
||||
#define SAVEFILE_HEADER_V2 "INFPNT000002"
|
||||
#define SAVEFILE_HEADER_V1 "INFPNT000001"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ void ServerData::save(cereal::PortableBinaryOutputArchive& a) const {
|
|||
a((uint64_t)components.size());
|
||||
for(uint64_t i = 0; i < components.size(); i++)
|
||||
a(components[i]->get_type(), components[i]->id, components[i]->coords, *components[i]);
|
||||
a(bookmarks);
|
||||
a(bookmarks, grids);
|
||||
}
|
||||
|
||||
void ServerData::load(cereal::PortableBinaryInputArchive& a) {
|
||||
|
|
@ -44,20 +44,7 @@ void ServerData::load(cereal::PortableBinaryInputArchive& a) {
|
|||
components.emplace_back(newComp);
|
||||
idToComponentMap.emplace(newComp->id, newComp);
|
||||
}
|
||||
a(bookmarks);
|
||||
}
|
||||
|
||||
ClientPortionID ServerData::get_max_id(ServerPortionID serverID) const {
|
||||
ClientPortionID maxClientID = 0;
|
||||
for(auto& p : resources) {
|
||||
if(p.first.first == serverID)
|
||||
maxClientID = std::max(maxClientID, p.first.second);
|
||||
}
|
||||
for(auto& p : components) {
|
||||
if(p->id.first == serverID)
|
||||
maxClientID = std::max(maxClientID, p->id.second);
|
||||
}
|
||||
return maxClientID;
|
||||
a(bookmarks, grids);
|
||||
}
|
||||
|
||||
MainServer::MainServer(World& initWorld, const std::string& serverLocalID):
|
||||
|
|
@ -69,6 +56,7 @@ MainServer::MainServer(World& initWorld, const std::string& serverLocalID):
|
|||
data.idToComponentMap.emplace(data.components.back()->id, data.components.back());
|
||||
}
|
||||
data.bookmarks = world.bMan.bookmark_list();
|
||||
data.grids = world.gridMan.grids;
|
||||
data.resources = world.rMan.resource_list();
|
||||
data.canvasBackColor = convert_vec3<Vector3f>(world.canvasTheme.backColor);
|
||||
|
||||
|
|
@ -91,23 +79,22 @@ MainServer::MainServer(World& initWorld, const std::string& serverLocalID):
|
|||
if(isDirectConnect)
|
||||
fileDisplayName = newClient.displayName;
|
||||
|
||||
newClient.id.first = Random::get().int_range<ServerPortionID>(1, std::numeric_limits<ServerPortionID>::max());
|
||||
client->customID = newClient.id.first;
|
||||
newClient.id.second = data.get_max_id(newClient.id.first);
|
||||
netServer->send_items_to_all_clients_except(client, RELIABLE_COMMAND_CHANNEL, CLIENT_USER_CONNECT, newClient.id.first, newClient.displayName, newClient.cursorColor);
|
||||
newClient.serverID = Random::get().int_range<ServerPortionID>(1, std::numeric_limits<ServerPortionID>::max());
|
||||
client->customID = newClient.serverID;
|
||||
netServer->send_items_to_all_clients_except(client, RELIABLE_COMMAND_CHANNEL, CLIENT_USER_CONNECT, newClient.serverID, newClient.displayName, newClient.cursorColor);
|
||||
if(isDirectConnect)
|
||||
netServer->send_items_to_client(client, RELIABLE_COMMAND_CHANNEL, CLIENT_INITIAL_DATA, isDirectConnect, newClient.id.first, newClient.cursorColor);
|
||||
netServer->send_items_to_client(client, RELIABLE_COMMAND_CHANNEL, CLIENT_INITIAL_DATA, isDirectConnect, newClient.serverID, newClient.cursorColor);
|
||||
else {
|
||||
netServer->send_items_to_client(client, RELIABLE_COMMAND_CHANNEL, CLIENT_INITIAL_DATA, isDirectConnect, newClient.id.first, newClient.cursorColor, newClient.displayName, fileDisplayName, clients, data);
|
||||
netServer->send_items_to_client(client, RELIABLE_COMMAND_CHANNEL, CLIENT_INITIAL_DATA, isDirectConnect, newClient.serverID, newClient.cursorColor, newClient.displayName, fileDisplayName, clients, data);
|
||||
for(auto& [id, rData] : data.resources)
|
||||
netServer->send_items_to_client(client, RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE, id, rData);
|
||||
}
|
||||
clients.emplace(newClient.id.first, newClient);
|
||||
clients.emplace(newClient.serverID, newClient);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_MOVE_MOUSE, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
auto& c = clients[client->customID];
|
||||
message(c.camCoords, c.windowSize, c.cursorPos);
|
||||
netServer->send_items_to_all_clients_except(client, UNRELIABLE_COMMAND_CHANNEL, CLIENT_MOVE_MOUSE, c.id.first, c.camCoords, c.windowSize, c.cursorPos);
|
||||
netServer->send_items_to_all_clients_except(client, UNRELIABLE_COMMAND_CHANNEL, CLIENT_MOVE_MOUSE, c.serverID, c.camCoords, c.windowSize, c.cursorPos);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_PLACE_SINGLE_COMPONENT, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
uint64_t placement;
|
||||
|
|
@ -247,6 +234,19 @@ MainServer::MainServer(World& initWorld, const std::string& serverLocalID):
|
|||
data.bookmarks.erase(name);
|
||||
netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_REMOVE_BOOKMARK, name);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_SET_GRID, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
ServerClientID gID;
|
||||
message(gID);
|
||||
WorldGrid& g = data.grids[gID];
|
||||
message(g);
|
||||
netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_SET_GRID, gID, g);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_REMOVE_GRID, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
ServerClientID gID;
|
||||
message(gID);
|
||||
data.grids.erase(gID);
|
||||
netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_REMOVE_GRID, gID);
|
||||
});
|
||||
netServer->add_recv_callback(SERVER_CANVAS_COLOR, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
message(data.canvasBackColor);
|
||||
netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_CANVAS_COLOR, data.canvasBackColor);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "../SharedTypes.hpp"
|
||||
#include "../BookmarkManager.hpp"
|
||||
#include "../ResourceManager.hpp"
|
||||
#include "../WorldGrid.hpp"
|
||||
|
||||
class DrawComponent;
|
||||
|
||||
|
|
@ -12,9 +13,10 @@ class ServerData {
|
|||
std::unordered_map<ServerClientID, std::shared_ptr<DrawComponent>> idToComponentMap;
|
||||
std::unordered_map<ServerClientID, ResourceData> resources;
|
||||
std::unordered_map<std::string, Bookmark> bookmarks;
|
||||
std::unordered_map<ServerClientID, WorldGrid> grids;
|
||||
|
||||
Vector3f canvasBackColor;
|
||||
void save(cereal::PortableBinaryOutputArchive& a) const;
|
||||
void write_to_file(cereal::PortableBinaryOutputArchive& a) const;
|
||||
void load(cereal::PortableBinaryInputArchive& a);
|
||||
ClientPortionID get_max_id(ServerPortionID serverID) const;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -587,6 +587,7 @@ void Toolbar::grid_menu(bool justOpened) {
|
|||
if(gui.svg_icon_button("visibility eye", grid.visible ? "data/icons/eyeopen.svg" : "data/icons/eyeclose.svg", false, entryHeight, false)) {
|
||||
miniButtonClicked = true;
|
||||
grid.visible = !grid.visible;
|
||||
main.world->gridMan.send_grid_info(gridID);
|
||||
}
|
||||
if(gui.svg_icon_button("edit pencil", "data/icons/pencil.svg", false, entryHeight, false)) {
|
||||
miniButtonClicked = true;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "Helpers/Networking/ByteStream.hpp"
|
||||
#include "Server/CommandList.hpp"
|
||||
#include "MainProgram.hpp"
|
||||
#include "SharedTypes.hpp"
|
||||
#include "WorldGrid.hpp"
|
||||
#include "cereal/archives/portable_binary.hpp"
|
||||
#include <fstream>
|
||||
|
|
@ -61,6 +62,7 @@ World::World(MainProgram& initMain, OpenWorldInfo& worldInfo):
|
|||
drawProg.init_client_callbacks();
|
||||
rMan.init_client_callbacks();
|
||||
bMan.init_client_callbacks();
|
||||
gridMan.init_client_callbacks();
|
||||
init_client_callbacks();
|
||||
con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_INITIAL_DATA, displayName, false);
|
||||
}
|
||||
|
|
@ -77,9 +79,9 @@ void World::init_client_callbacks() {
|
|||
message(newBackColor);
|
||||
set_canvas_background_color(newBackColor, false);
|
||||
drawProg.initialize_draw_data(message);
|
||||
message(bMan);
|
||||
message(bMan, gridMan);
|
||||
}
|
||||
nextClientID = std::max(rMan.get_max_id(ownID), drawProg.get_max_id(ownID));
|
||||
nextClientID = get_max_id(ownID);
|
||||
clientStillConnecting = false;
|
||||
});
|
||||
con.client_add_recv_callback(CLIENT_USER_CONNECT, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
|
|
@ -240,6 +242,7 @@ void World::start_hosting(const std::string& initNetSource, const std::string& s
|
|||
drawProg.init_client_callbacks();
|
||||
rMan.init_client_callbacks();
|
||||
bMan.init_client_callbacks();
|
||||
gridMan.init_client_callbacks();
|
||||
init_client_callbacks();
|
||||
con.client_send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_INITIAL_DATA, displayName, true);
|
||||
conType = CONNECTIONTYPE_SERVER;
|
||||
|
|
@ -302,13 +305,13 @@ void World::load_from_file(const std::filesystem::path& fileName, std::string_vi
|
|||
}
|
||||
|
||||
if(buffer.size() < SAVEFILE_HEADER_LEN)
|
||||
throw std::runtime_error("[World::load_from_file] File too small");
|
||||
throw std::runtime_error("[World::load_from_file] File is not an InfiniPaint canvas (File too small)");
|
||||
|
||||
std::string_view fileHeader = buffer.substr(0, SAVEFILE_HEADER_LEN);
|
||||
std::string_view uncompressedDataView;
|
||||
std::vector<char> uncompressedDataVector;
|
||||
|
||||
if(fileHeader == SAVEFILE_HEADER) {
|
||||
if(fileHeader == SAVEFILE_HEADER || fileHeader == SAVEFILE_HEADER_V2) {
|
||||
uncompressedDataVector.resize(ZSTD_getFrameContentSize(buffer.data() + SAVEFILE_HEADER_LEN, buffer.size() - SAVEFILE_HEADER_LEN));
|
||||
size_t trueUncompressedSize = ZSTD_decompress(uncompressedDataVector.data(), uncompressedDataVector.size(), buffer.data() + SAVEFILE_HEADER_LEN, buffer.size() - SAVEFILE_HEADER_LEN);
|
||||
uncompressedDataView = std::string_view(uncompressedDataVector.data(), trueUncompressedSize);
|
||||
|
|
@ -316,7 +319,7 @@ void World::load_from_file(const std::filesystem::path& fileName, std::string_vi
|
|||
else if(fileHeader == SAVEFILE_HEADER_V1)
|
||||
uncompressedDataView = std::string_view(buffer.data() + SAVEFILE_HEADER_LEN, buffer.size() - SAVEFILE_HEADER_LEN);
|
||||
else
|
||||
throw std::runtime_error("[World::load_from_file] File does not have correct header");
|
||||
throw std::runtime_error("[World::load_from_file] File is not an InfiniPaint canvas (Wrong header)");
|
||||
|
||||
|
||||
|
||||
|
|
@ -325,17 +328,22 @@ void World::load_from_file(const std::filesystem::path& fileName, std::string_vi
|
|||
cereal::PortableBinaryInputArchive a(f);
|
||||
SaveLoadFileOp op{.world = this, .versionStr = std::string(fileHeader)};
|
||||
a(op);
|
||||
nextClientID = std::max(rMan.get_max_id(ownID), drawProg.get_max_id(ownID));
|
||||
nextClientID = get_max_id(ownID);
|
||||
|
||||
Logger::get().log("USERINFO", "File loaded");
|
||||
set_name(filePath.stem().string());
|
||||
}
|
||||
|
||||
ClientPortionID World::get_max_id(ServerPortionID serverID) {
|
||||
return std::max(std::max(rMan.get_max_id(serverID), drawProg.get_max_id(serverID)), gridMan.get_max_id(serverID));
|
||||
}
|
||||
|
||||
void World::SaveLoadFileOp::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(world->drawData.cam.c, world->main.window.size.cast<float>().eval());
|
||||
a(convert_vec3<Vector3f>(world->canvasTheme.backColor));
|
||||
world->drawProg.write_to_file(a);
|
||||
a(world->bMan);
|
||||
a(world->gridMan);
|
||||
world->rMan.save_strip_unused_resources(a, world->drawProg.get_used_resources());
|
||||
}
|
||||
|
||||
|
|
@ -343,16 +351,20 @@ void World::SaveLoadFileOp::load(cereal::PortableBinaryInputArchive& a) {
|
|||
CoordSpaceHelper coordsToJumpTo;
|
||||
Vector2f windowSizeToJumpTo;
|
||||
a(coordsToJumpTo, windowSizeToJumpTo);
|
||||
if(versionStr == SAVEFILE_HEADER) {
|
||||
|
||||
if(versionStr == SAVEFILE_HEADER_V1)
|
||||
world->set_canvas_background_color(DEFAULT_CANVAS_BACKGROUND_COLOR);
|
||||
else {
|
||||
Vector3f canvasBackColor;
|
||||
a(canvasBackColor);
|
||||
world->set_canvas_background_color(canvasBackColor);
|
||||
}
|
||||
else
|
||||
world->set_canvas_background_color(DEFAULT_CANVAS_BACKGROUND_COLOR);
|
||||
|
||||
world->drawData.cam.smooth_move_to(*world, coordsToJumpTo, windowSizeToJumpTo, true);
|
||||
world->drawProg.initialize_draw_data(a);
|
||||
a(world->bMan);
|
||||
if(versionStr == SAVEFILE_HEADER)
|
||||
a(world->gridMan);
|
||||
a(world->rMan);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ class World {
|
|||
} canvasTheme;
|
||||
|
||||
private:
|
||||
ClientPortionID get_max_id(ServerPortionID serverID);
|
||||
|
||||
void init_client_callbacks();
|
||||
void set_name(const std::string& n);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,20 +2,25 @@
|
|||
#include <include/core/SkCanvas.h>
|
||||
#include "DrawData.hpp"
|
||||
#include <include/effects/SkRuntimeEffect.h>
|
||||
#include <cereal/types/optional.hpp>
|
||||
|
||||
class GridManager;
|
||||
|
||||
class WorldGrid {
|
||||
public:
|
||||
template <class Archive> void serialize(Archive& a) {
|
||||
a(name, gridType, size, visible, offset, bounds, color, removeDivisionsOutwards, subdivisions);
|
||||
}
|
||||
|
||||
void draw(GridManager& gMan, SkCanvas* canvas, const DrawData& drawData);
|
||||
|
||||
std::string name;
|
||||
enum class GridType : unsigned {
|
||||
CIRCLE_POINTS = 0,
|
||||
SQUARE_POINTS,
|
||||
SQUARE_LINES,
|
||||
RULED
|
||||
} gridType = GridType::CIRCLE_POINTS;
|
||||
std::string name;
|
||||
WorldScalar size{50000000};
|
||||
bool visible = true;
|
||||
WorldVec offset{0, 0};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue