infinipaint/src/World.cpp

726 lines
29 KiB
C++
Raw Normal View History

/*
* InfiniPaint
* Copyright (C) 2025-2026 Yousef Khadadeh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
2025-07-06 20:43:53 +03:00
#include "World.hpp"
2025-12-10 17:23:05 +03:00
#include <Helpers/HsvRgb.hpp>
#include <Helpers/MathExtras.hpp>
#include <Helpers/Networking/ByteStream.hpp>
#include <Helpers/NetworkingObjects/NetObjManager.hpp>
#include <Helpers/NetworkingObjects/NetObjManagerTypeList.hpp>
#include <Helpers/VersionNumber.hpp>
#include <Helpers/NetworkingObjects/NetObjGenericSerializedClass.hpp>
#include <Helpers/NetworkingObjects/DelayUpdateSerializedClassManager.hpp>
2025-12-16 12:02:08 +03:00
#include <cereal/types/unordered_map.hpp>
2025-12-25 17:18:28 +03:00
#include "DrawingProgram/Layers/DrawingProgramLayerListItem.hpp"
#include "Helpers/NetworkingObjects/NetObjOrderedList.hpp"
#include "Helpers/NetworkingObjects/NetObjTemporaryPtr.decl.hpp"
#include "Helpers/NetworkingObjects/NetObjUnorderedSet.hpp"
#include "CommandList.hpp"
2026-01-18 13:39:49 +03:00
#include "Helpers/StringHelpers.hpp"
2025-07-06 20:43:53 +03:00
#include "MainProgram.hpp"
#include "SharedTypes.hpp"
2025-10-16 09:38:58 +03:00
#include "Toolbar.hpp"
2025-11-16 16:39:12 +03:00
#include "VersionConstants.hpp"
2025-09-12 09:54:49 +03:00
#include "WorldGrid.hpp"
#include <cereal/archives/portable_binary.hpp>
#include <chrono>
2025-07-06 20:43:53 +03:00
#include <include/core/SkFontMetrics.h>
#include <Helpers/Networking/NetLibrary.hpp>
#include <Helpers/Logger.hpp>
#include <cereal/types/vector.hpp>
#include <Helpers/Networking/NetLibrary.hpp>
2025-07-28 18:21:09 +03:00
#include <zstd.h>
2025-12-15 16:49:51 +03:00
#include "CanvasComponents/CanvasComponent.hpp"
#include "CanvasComponents/CanvasComponentContainer.hpp"
#include "CanvasComponents/CanvasComponentAllocator.hpp"
#include "WorldScreenshot.hpp"
2026-05-20 16:01:55 +03:00
#include "FileHelpers.hpp"
2025-07-06 20:43:53 +03:00
#ifdef __EMSCRIPTEN__
#include <EmscriptenHelpers/emscripten_browser_file.h>
#endif
World::World(MainProgram& initMain, const CustomEvents::OpenInfiniPaintFileEvent& worldInfo):
2026-01-01 22:46:02 +03:00
netObjMan(!worldInfo.isClient),
2025-07-06 20:43:53 +03:00
main(initMain),
2026-01-02 17:11:32 +03:00
undo(*this),
2025-07-06 20:43:53 +03:00
rMan(*this),
drawProg(*this),
2025-09-06 19:06:30 +03:00
bMan(*this),
2025-12-16 23:06:46 +03:00
gridMan(*this),
canvasTheme(*this)
2025-07-06 20:43:53 +03:00
{
saveThumbnail = worldInfo.saveThumbnail;
2025-12-10 17:23:05 +03:00
init_net_obj_type_list();
2026-01-02 17:11:32 +03:00
netObjMan.set_netobj_destroy_callback([&undo = undo](const NetworkingObjects::NetObjID& idToDestroy) {
undo.remove_by_netid(idToDestroy);
});
netObjMan.set_netid_reassign_callback([&undo = undo](const NetworkingObjects::NetObjID& oldID, const NetworkingObjects::NetObjID& newID) {
undo.reassign_netid(oldID, newID);
});
2025-12-10 17:23:05 +03:00
2025-07-06 20:43:53 +03:00
netSource = worldInfo.netSource;
drawData.rMan = &rMan;
drawData.main = &main;
drawData.cam.set_viewing_area(main.window.size.cast<float>());
2025-07-06 20:43:53 +03:00
2026-01-01 22:46:02 +03:00
if(worldInfo.isClient)
init_client(worldInfo.netSource);
else {
init_client_data_list();
set_name(name);
2026-01-01 22:46:02 +03:00
if(worldInfo.filePathSource.has_value())
load_from_file(worldInfo.filePathSource.value(), worldInfo.fileDataBuffer);
else
load_empty_canvas(worldInfo.filePathEmptyAutoSaveDir);
#ifdef ENABLE_ORDERED_LIST_TEST
listDebugTest = netObjMan.make_obj<NetworkingObjects::NetObjOrderedList<uint16_t>>();
#endif
2025-07-06 20:43:53 +03:00
}
}
void World::init_client_data_list() {
2026-01-01 22:46:02 +03:00
clients = netObjMan.make_obj<NetworkingObjects::NetObjUnorderedSet<ClientData>>();
2026-05-30 20:57:50 +03:00
serverClientData = ownClientData = clients->emplace_direct(clients, ClientData::InitStruct{
2026-01-01 22:46:02 +03:00
.cursorColor = get_random_cursor_color(),
.displayName = main.conf.displayName
2026-01-01 22:46:02 +03:00
});
init_client_data_list_callbacks();
}
Vector3f World::get_random_cursor_color() {
Vector3f hsv;
hsv[0] = Random::get().real_range(0.0f, 360.0f);
hsv[1] = Random::get().real_range(0.3f, 0.7f);
hsv[2] = 1.0;
return hsv_to_rgb<Vector3f>(hsv);
}
void World::init_client_data_list_callbacks() {
clients->set_insert_callback([&](const NetworkingObjects::NetObjOwnerPtr<ClientData>& objPtr) {
add_chat_message(objPtr->get_display_name(), "joined", Toolbar::ChatMessage::Type::JOIN);
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::PHONE_USERINFO, objPtr->get_display_name() + " joined");
});
clients->set_erase_callback([&](const NetworkingObjects::NetObjOwnerPtr<ClientData>& objPtr) {
add_chat_message(objPtr->get_display_name(), "left", Toolbar::ChatMessage::Type::JOIN);
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::PHONE_USERINFO, objPtr->get_display_name() + " left");
});
2025-07-06 20:43:53 +03:00
}
2025-12-10 17:23:05 +03:00
void World::init_net_obj_type_list() {
BookmarkListItem::register_class(*this);
2025-12-31 16:30:56 +03:00
WorldGrid::register_class(*this);
2025-12-13 13:03:27 +03:00
NetworkingObjects::register_ordered_list_class<WorldGrid>(netObjMan);
CanvasComponentAllocator::register_class(*this);
2025-12-31 16:30:56 +03:00
CanvasComponentContainer::register_class(*this);
2025-12-15 16:49:51 +03:00
NetworkingObjects::register_ordered_list_class<CanvasComponentContainer>(netObjMan);
2025-12-25 17:18:28 +03:00
DrawingProgramLayerListItem::register_class(*this);
NetworkingObjects::register_ordered_list_class<DrawingProgramLayerListItem>(netObjMan);
2025-12-16 23:06:46 +03:00
canvasTheme.register_class();
ClientData::register_class(*this);
NetworkingObjects::register_unordered_set_class<ClientData>(netObjMan);
#ifdef ENABLE_ORDERED_LIST_TEST
NetworkingObjects::register_generic_serialized_class<uint16_t>(netObjMan);
NetworkingObjects::register_ordered_list_class<uint16_t>(netObjMan);
#endif
2025-12-16 23:06:46 +03:00
}
2026-01-01 22:46:02 +03:00
void World::init_client(const std::string& serverFullID) {
main.init_net_library();
clientStillConnecting = true;
netClient = std::make_shared<NetClient>(serverFullID);
lastKeepAliveSent = std::chrono::steady_clock::now();
NetLibrary::register_client(netClient);
2026-06-02 21:57:53 +03:00
netObjMan.set_client(netClient, SERVER_UPDATE_NETWORK_OBJECT);
2026-01-01 22:46:02 +03:00
set_name("");
2025-12-10 17:23:05 +03:00
rMan.init_client_callbacks();
2025-12-31 11:42:16 +03:00
drawProg.init_client_callbacks();
2026-01-01 22:46:02 +03:00
netClient->add_recv_callback(CLIENT_INITIAL_DATA, [&](cereal::PortableBinaryInputArchive& message) {
std::string fileDisplayName;
2026-05-30 20:57:50 +03:00
NetworkingObjects::NetObjID clientDataObjID, serverDataObjID;
message(fileDisplayName, clientDataObjID, serverDataObjID);
set_name(fileDisplayName);
2026-05-30 20:57:50 +03:00
clients = netObjMan.read_create_message<NetworkingObjects::NetObjUnorderedSet<ClientData>>(message, nullptr);
ownClientData = netObjMan.get_obj_temporary_ref_from_id<ClientData>(clientDataObjID);
serverClientData = netObjMan.get_obj_temporary_ref_from_id<ClientData>(serverDataObjID);
2026-04-09 12:00:36 +03:00
bMan.read_create_message(message);
gridMan.read_create_message(message);
drawProg.read_components_client(message);
canvasTheme.read_create_message(message);
init_client_data_list_callbacks();
drawData.cam.smooth_move_to(*main.world, ownClientData->get_cam_coords(), ownClientData->get_window_size(), true);
#ifdef ENABLE_ORDERED_LIST_TEST
listDebugTest = netObjMan.read_create_message<NetworkingObjects::NetObjOrderedList<uint16_t>>(message, nullptr);
#endif
2025-07-06 20:43:53 +03:00
clientStillConnecting = false;
2026-04-09 12:00:36 +03:00
set_to_layout_gui_if_focus();
2025-07-06 20:43:53 +03:00
});
2026-01-01 22:46:02 +03:00
netClient->add_recv_callback(CLIENT_UPDATE_NETWORK_OBJECT, [&](cereal::PortableBinaryInputArchive& message) {
netObjMan.read_update_message(message, nullptr);
2025-09-30 16:36:09 +03:00
});
2026-01-01 22:46:02 +03:00
netClient->add_recv_callback(CLIENT_KEEP_ALIVE, [&](cereal::PortableBinaryInputArchive& message) {
2025-07-06 20:43:53 +03:00
});
2026-01-01 22:46:02 +03:00
netClient->send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_INITIAL_DATA, main.conf.displayName);
2025-07-06 20:43:53 +03:00
}
2026-06-02 21:57:53 +03:00
void World::send_reliable_multi_command_to_all(const std::function<void()>& captureSendBlock) {
if(netServer)
netServer->send_multi_command_to_all_clients(RELIABLE_COMMAND_CHANNEL, captureSendBlock);
else if(netClient)
netClient->send_multi_command_to_server(RELIABLE_COMMAND_CHANNEL, captureSendBlock);
else
captureSendBlock();
}
2025-07-06 20:43:53 +03:00
void World::focus_update() {
if(!clientStillConnecting) {
delayedUpdateObjectManager.update(netObjMan);
constexpr float SECONDS_TO_SEND_CAMERA_DATA = 0.5f;
timeToSendCameraData.update_time_since();
if(timeToSendCameraData.get_time_since() > SECONDS_TO_SEND_CAMERA_DATA) {
timeToSendCameraData.update_time_point();
ownClientData->set_window_size(ownClientData, main.window.size.cast<float>().eval());
ownClientData->set_camera_coords(ownClientData, drawData.cam.c);
}
ownClientData->set_cursor_pos(ownClientData, main.input.mouse.pos);
2025-07-06 20:43:53 +03:00
drawProg.update();
#ifdef ENABLE_ORDERED_LIST_TEST
list_debug_test_update();
#endif
2025-07-06 20:43:53 +03:00
}
drawData.cam.update_main(*this);
rMan.update();
}
2026-04-05 20:26:03 +03:00
bool World::connection_update() {
2026-01-01 22:46:02 +03:00
if(netServer) {
if(netServer->is_disconnected()) {
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::USERINFO, "Host connection failed");
2026-01-01 22:46:02 +03:00
netSource.clear();
netServer = nullptr;
netObjMan.disconnect();
}
else {
netServer->update();
if(std::chrono::steady_clock::now() - lastKeepAliveSent > std::chrono::seconds(2)) {
netServer->send_items_to_all_clients(UNRELIABLE_COMMAND_CHANNEL, CLIENT_KEEP_ALIVE);
lastKeepAliveSent = std::chrono::steady_clock::now();
}
}
}
else if(netClient) {
if(netClient->is_disconnected()) {
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::USERINFO, "Client connection failed");
2026-01-01 22:46:02 +03:00
netObjMan.disconnect();
2026-04-05 20:26:03 +03:00
main.set_tab_to_close(this);
2026-05-14 11:53:36 +03:00
netClient = nullptr;
2026-04-05 20:26:03 +03:00
return false;
2026-01-01 22:46:02 +03:00
}
netClient->update();
if(std::chrono::steady_clock::now() - lastKeepAliveSent > std::chrono::seconds(2)) {
netClient->send_items_to_server(UNRELIABLE_COMMAND_CHANNEL, SERVER_KEEP_ALIVE);
lastKeepAliveSent = std::chrono::steady_clock::now();
}
}
2026-04-05 20:26:03 +03:00
return true;
2026-01-01 22:46:02 +03:00
}
bool World::is_focus() {
return main.world.get() == this;
}
2026-04-09 12:00:36 +03:00
void World::set_to_layout_gui_if_focus() {
if(is_focus())
main.g.gui.set_to_layout();
}
void World::undo_with_checks() {
if(!clientStillConnecting && !drawProg.prevent_undo_or_redo())
undo.undo();
}
void World::redo_with_checks() {
if(!clientStillConnecting && !drawProg.prevent_undo_or_redo())
undo.redo();
2025-07-06 20:43:53 +03:00
}
2026-04-12 12:36:00 +03:00
void World::update() {
2026-01-01 22:46:02 +03:00
connection_update();
2025-07-06 20:43:53 +03:00
}
void World::on_tab_out() {
rMan.clear_display_cache();
2026-04-12 17:31:32 +03:00
if(!clientStillConnecting)
drawProg.on_tab_out();
}
void World::input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile) {
if(!clientStillConnecting)
drawProg.input_add_file_to_canvas_callback(addFile);
}
void World::input_paste_callback(const CustomEvents::PasteEvent& paste) {
if(!clientStillConnecting)
drawProg.input_paste_callback(paste);
}
void World::input_android_text_box_input_callback(const CustomEvents::AndroidTextBoxInputEvent& textboxInput) {
if(!clientStillConnecting)
drawProg.input_android_text_box_input_callback(textboxInput);
}
void World::input_text_key_callback(const InputManager::KeyCallbackArgs& key) {
if(!clientStillConnecting)
drawProg.input_text_key_callback(key);
}
void World::input_text_callback(const InputManager::TextCallbackArgs& text) {
if(!clientStillConnecting)
drawProg.input_text_callback(text);
}
void World::input_drop_file_callback(const InputManager::DropCallbackArgs& drop) {
if(!clientStillConnecting)
drawProg.input_drop_file_callback(drop);
}
void World::input_drop_text_callback(const InputManager::DropCallbackArgs& drop) {
if(!clientStillConnecting)
drawProg.input_drop_text_callback(drop);
}
void World::input_key_callback(const InputManager::KeyCallbackArgs& key) {
if(!clientStillConnecting) {
switch(key.key) {
case InputManager::KEY_REDO: {
if(key.down)
redo_with_checks();
break;
}
case InputManager::KEY_UNDO: {
if(key.down)
undo_with_checks();
break;
}
}
drawProg.input_key_callback(key);
2026-03-02 23:09:48 +03:00
drawData.cam.input_key_callback(key);
}
}
void World::input_mouse_button_callback(const InputManager::MouseButtonCallbackArgs& button) {
2026-03-03 22:31:48 +03:00
if(!clientStillConnecting) {
drawProg.input_mouse_button_callback(button);
2026-03-03 22:31:48 +03:00
drawData.cam.input_mouse_button_on_canvas_callback(*this, button);
}
}
void World::input_mouse_motion_callback(const InputManager::MouseMotionCallbackArgs& motion) {
2026-03-02 23:09:48 +03:00
if(!clientStillConnecting) {
drawProg.input_mouse_motion_callback(motion);
2026-03-02 23:09:48 +03:00
drawData.cam.input_mouse_motion_callback(*this, motion);
}
}
void World::input_mouse_wheel_callback(const InputManager::MouseWheelCallbackArgs& wheel) {
2026-03-03 16:01:21 +03:00
if(!clientStillConnecting)
2026-03-02 23:09:48 +03:00
drawData.cam.input_mouse_wheel_callback(*this, wheel);
2026-03-03 16:01:21 +03:00
}
void World::input_pen_button_callback(const InputManager::PenButtonCallbackArgs& button) {
if(!clientStillConnecting)
drawProg.input_pen_button_callback(button);
}
void World::input_pen_touch_callback(const InputManager::PenTouchCallbackArgs& touch) {
if(!clientStillConnecting)
drawProg.input_pen_touch_callback(touch);
}
void World::input_pen_motion_callback(const InputManager::PenMotionCallbackArgs& motion) {
if(!clientStillConnecting)
drawProg.input_pen_motion_callback(motion);
}
void World::input_pen_axis_callback(const InputManager::PenAxisCallbackArgs& axis) {
if(!clientStillConnecting)
drawProg.input_pen_axis_callback(axis);
}
void World::input_multi_finger_touch_callback(const InputManager::MultiFingerTouchCallbackArgs& touch) {
if(!clientStillConnecting)
drawData.cam.input_multi_finger_touch_callback(*this, touch);
}
void World::input_multi_finger_motion_callback(const InputManager::MultiFingerMotionCallbackArgs& motion) {
if(!clientStillConnecting)
drawData.cam.input_multi_finger_motion_callback(*this, motion);
}
std::optional<InputManager::TextBoxStartInfo> World::get_text_box_start_info() {
if(!clientStillConnecting)
return drawProg.get_text_box_start_info();
return std::nullopt;
}
2025-07-06 20:43:53 +03:00
void World::send_chat_message(const std::string& message) {
if(!clientStillConnecting)
ownClientData->send_chat_message(ownClientData, *this, message);
2025-07-06 20:43:53 +03:00
}
2025-10-16 09:38:58 +03:00
void World::add_chat_message(const std::string& name, const std::string& message, Toolbar::ChatMessage::Type type) {
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::CHAT, type == Toolbar::ChatMessage::JOIN ? (name + " " + message) : ("[" + name + "] " + message));
2025-10-16 09:38:58 +03:00
chatMessages.emplace_front(Toolbar::ChatMessage{name, message, type});
2025-07-06 20:43:53 +03:00
if(chatMessages.size() == CHAT_SIZE)
chatMessages.pop_back();
2026-04-09 12:00:36 +03:00
set_to_layout_gui_if_focus();
2025-07-06 20:43:53 +03:00
}
void World::early_destroy() {
//con.early_destroy();
}
void World::set_name(const std::string& n) {
if(n.empty())
name = "New File";
else
name = n;
}
void World::ensure_display_name_unique(std::string& displayName) {
2026-01-18 13:39:49 +03:00
std::vector<std::string> strList;
for(auto& client : clients->get_data())
strList.emplace_back(client->get_display_name());
displayName = ensure_string_unique(strList, displayName);
}
2025-07-06 20:43:53 +03:00
void World::start_hosting(const std::string& initNetSource, const std::string& serverLocalID) {
main.init_net_library();
netServer = std::make_shared<NetServer>(serverLocalID);
2026-01-01 22:46:02 +03:00
lastKeepAliveSent = std::chrono::steady_clock::now();
NetLibrary::register_server(netServer);
2026-06-02 21:57:53 +03:00
netObjMan.set_server(netServer, CLIENT_UPDATE_NETWORK_OBJECT);
2025-07-06 20:43:53 +03:00
netSource = initNetSource;
2026-01-01 22:46:02 +03:00
rMan.init_server_callbacks();
drawProg.init_server_callbacks();
netServer->add_recv_callback(SERVER_INITIAL_DATA, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
ClientData::InitStruct newClientData;
newClientData.cursorColor = get_random_cursor_color();
message(newClientData.displayName);
ensure_display_name_unique(newClientData.displayName);
newClientData.camCoords = ownClientData->get_cam_coords();
newClientData.windowSize = ownClientData->get_window_size();
newClientData.gridSize = ownClientData->get_grid_size();
NetworkingObjects::NetObjTemporaryPtr<ClientData> clientDataObjPtr = clients->emplace_direct(clients, newClientData);
client->customID = clientDataObjPtr.get_net_id().data;
auto ss(std::make_shared<std::stringstream>());
{
cereal::PortableBinaryOutputArchive a(*ss);
2026-05-30 20:57:50 +03:00
a(CLIENT_INITIAL_DATA, name, clientDataObjPtr.get_net_id(), serverClientData.get_net_id());
clients.write_create_message(a);
2026-01-01 22:46:02 +03:00
bMan.bookmarkListRoot.write_create_message(a);
gridMan.grids.write_create_message(a);
drawProg.write_components_server(a);
canvasTheme.write_create_message(a);
#ifdef ENABLE_ORDERED_LIST_TEST
listDebugTest.write_create_message(a);
#endif
2026-01-01 22:46:02 +03:00
}
netServer->send_string_stream_to_client(client, RELIABLE_COMMAND_CHANNEL, ss);
for(auto& r : rMan.resource_list()) {
netServer->send_items_to_client(client, RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE_ID, r.get_net_id());
netServer->send_items_to_client(client, RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE_DATA, *r);
}
});
netServer->add_recv_callback(SERVER_UPDATE_NETWORK_OBJECT, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
netObjMan.read_update_message(message, client);
});
netServer->add_recv_callback(SERVER_KEEP_ALIVE, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
});
netServer->add_disconnect_callback([&](std::shared_ptr<NetServer::ClientData> client) {
NetworkingObjects::NetObjID idToErase;
idToErase.data = client->customID;
clients->erase(clients, idToErase);
});
2025-07-06 20:43:53 +03:00
}
2026-01-18 13:39:49 +03:00
void World::autosave_to_directory(const std::filesystem::path& directoryToSaveAt) {
std::vector<std::string> strList;
try {
strList = glob_path_as_string_list(directoryToSaveAt, ("*" + World::DOT_FILE_EXTENSION).c_str(), 0, [&](const auto& p){ return p.stem().string();});
}
catch(...) {
2026-01-18 13:39:49 +03:00
}
std::string nameToSaveUnder = ensure_string_unique(strList, name);
2026-05-12 15:19:43 +03:00
save_to_file(directoryToSaveAt / std::filesystem::path(nameToSaveUnder + "." + FILE_EXTENSION));
2026-01-18 13:39:49 +03:00
}
void World::save_to_file(const std::filesystem::path& filePathToSaveAt) {
2025-07-06 20:43:53 +03:00
try {
2026-05-20 16:01:55 +03:00
if(main.conf.forceExtensionOnPath)
filePath = force_extension_on_path(filePathToSaveAt, FILE_EXTENSION);
else
filePath = filePathToSaveAt;
2025-07-06 20:43:53 +03:00
std::stringstream f;
2025-11-16 16:39:12 +03:00
f.write(VersionConstants::CURRENT_SAVEFILE_HEADER.c_str(), VersionConstants::SAVEFILE_HEADER_LEN);
2025-07-28 18:21:09 +03:00
{
std::stringstream fWorldDataToCompress;
{
cereal::PortableBinaryOutputArchive a(fWorldDataToCompress);
2025-11-16 16:39:12 +03:00
save_file(a);
2025-07-28 18:21:09 +03:00
}
std::vector<char> compressedData(ZSTD_compressBound(fWorldDataToCompress.view().size()));
size_t trueCompressedSize = ZSTD_compress(compressedData.data(), compressedData.size(), fWorldDataToCompress.view().data(), fWorldDataToCompress.view().size(), ZSTD_CLEVEL_DEFAULT);
std::string_view compressedF(compressedData.data(), trueCompressedSize);
f << compressedF;
}
2025-07-06 20:43:53 +03:00
set_name(filePath.stem().string());
#ifdef __EMSCRIPTEN__
emscripten_browser_file::download(
filePath.string(),
"application/octet-stream",
f.view()
);
2025-07-28 18:21:09 +03:00
#else
2026-04-11 11:17:26 +03:00
if(!SDL_SaveFile(filePath.string().c_str(), f.view().data(), f.view().size()))
throw std::runtime_error("SDL_SaveFile failed with error: " + std::string(SDL_GetError()));
else if(saveThumbnail) {
Vector2f imageCenter{main.window.size.x() * 0.5f, main.window.size.y() * 0.5f};
float imageDim = std::max(main.window.size.x(), main.window.size.y());
Vector2f imageDimVec{imageDim * 0.5f, imageDim * 0.5f};
SCollision::AABB<float> imageBounds{imageCenter - imageDimVec, imageCenter + imageDimVec};
world_take_screenshot(main.world, {
.filePath = filePath.parent_path() / (filePath.stem().string() + ".jpg"),
.type = WorldScreenshotInfo::ScreenshotType::JPG,
.imageSizePixels = {512, 512},
.cameraCoords = drawData.cam.c,
.imageBounds = imageBounds,
.transparentBackground = false,
.displayGrid = false
});
}
2025-07-06 20:43:53 +03:00
#endif
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "File saved");
undo.set_save_action();
2025-07-06 20:43:53 +03:00
}
catch(const std::exception& e) {
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::WORLDFATAL, std::string("Save error: ") + e.what());
2025-07-06 20:43:53 +03:00
}
}
void World::load_empty_canvas(const std::optional<std::filesystem::path>& filePathEmptyAutoSaveDir) {
2026-01-01 22:46:02 +03:00
gridMan.server_init_no_file();
bMan.server_init_no_file();
2026-06-04 19:35:56 +03:00
BookmarkListItem* originBookmark = new BookmarkListItem(netObjMan, "Origin", false, {
.coords = drawData.cam.c,
.windowSize = main.window.size
});
bMan.bookmarkListRoot->get_folder_list()->push_back_and_send_create(bMan.bookmarkListRoot->get_folder_list(), originBookmark);
2026-01-01 22:46:02 +03:00
drawProg.server_init_no_file();
canvasTheme.server_init_no_file();
2026-05-13 14:31:35 +03:00
if(filePathEmptyAutoSaveDir.has_value()) {
bool oldSaveThumbnail = true;
saveThumbnail = false;
autosave_to_directory(filePathEmptyAutoSaveDir.value());
2026-05-13 14:31:35 +03:00
saveThumbnail = oldSaveThumbnail;
}
2026-01-01 22:46:02 +03:00
}
void World::load_from_file(const std::filesystem::path& filePathToLoadFrom, std::string_view buffer) {
filePath = filePathToLoadFrom;
2025-07-06 20:43:53 +03:00
2025-07-28 18:21:09 +03:00
std::string byteDataFromFile;
2025-07-20 15:24:44 +03:00
2025-07-06 20:43:53 +03:00
if(buffer.empty()) {
byteDataFromFile = read_file_to_string(filePath);
2025-07-28 18:21:09 +03:00
buffer = byteDataFromFile;
2025-07-06 20:43:53 +03:00
}
2025-07-28 18:21:09 +03:00
2025-11-16 16:39:12 +03:00
if(buffer.size() < VersionConstants::SAVEFILE_HEADER_LEN)
throw std::runtime_error("[World::load_from_file] File is not an InfiniPaint canvas (File too small)");
2025-07-28 18:21:09 +03:00
2025-11-16 16:39:12 +03:00
std::string_view fileHeader = buffer.substr(0, VersionConstants::SAVEFILE_HEADER_LEN);
VersionNumber fileVersion = VersionConstants::header_to_version_number(std::string(fileHeader));
2025-07-28 18:21:09 +03:00
std::string_view uncompressedDataView;
std::vector<char> uncompressedDataVector;
2025-11-16 16:39:12 +03:00
if(fileVersion < VersionNumber(0, 1, 0))
uncompressedDataView = std::string_view(buffer.data() + VersionConstants::SAVEFILE_HEADER_LEN, buffer.size() - VersionConstants::SAVEFILE_HEADER_LEN);
else {
uncompressedDataVector.resize(ZSTD_getFrameContentSize(buffer.data() + VersionConstants::SAVEFILE_HEADER_LEN, buffer.size() - VersionConstants::SAVEFILE_HEADER_LEN));
size_t trueUncompressedSize = ZSTD_decompress(uncompressedDataVector.data(), uncompressedDataVector.size(), buffer.data() + VersionConstants::SAVEFILE_HEADER_LEN, buffer.size() - VersionConstants::SAVEFILE_HEADER_LEN);
2025-07-28 18:21:09 +03:00
uncompressedDataView = std::string_view(uncompressedDataVector.data(), trueUncompressedSize);
2025-07-06 20:43:53 +03:00
}
2025-07-28 18:21:09 +03:00
ByteMemStream f((char*)uncompressedDataView.data(), uncompressedDataView.size());
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::INFO, "Loading file from version " + version_numbers_to_version_str(fileVersion));
2025-07-28 18:21:09 +03:00
cereal::PortableBinaryInputArchive a(f);
2025-11-16 16:39:12 +03:00
load_file(a, fileVersion);
2025-07-06 20:43:53 +03:00
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "File loaded");
2025-07-06 20:43:53 +03:00
set_name(filePath.stem().string());
}
2025-11-16 16:39:12 +03:00
void World::save_file(cereal::PortableBinaryOutputArchive& a) const {
2026-01-01 22:46:02 +03:00
drawData.cam.save_file(a, *this);
canvasTheme.save_file(a);
2025-11-16 16:39:12 +03:00
drawProg.save_file(a);
2026-01-01 22:46:02 +03:00
bMan.save_file(a);
gridMan.save_file(a);
2026-01-01 23:28:26 +03:00
rMan.save_file(a);
2025-07-06 20:43:53 +03:00
}
2025-11-16 16:39:12 +03:00
void World::load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version) {
2026-01-01 22:46:02 +03:00
drawData.cam.load_file(a, version, *this);
canvasTheme.load_file(a, version);
2025-11-16 16:39:12 +03:00
drawProg.load_file(a, version);
2026-01-01 22:46:02 +03:00
bMan.load_file(a, version);
gridMan.load_file(a, version);
2026-01-01 23:28:26 +03:00
rMan.load_file(a, version);
2025-07-06 20:43:53 +03:00
}
WorldScalar World::calculate_zoom_from_uniform_zoom(WorldScalar uniformZoom, WorldVec oldWindowSize) {
WorldScalar a1(main.window.size.x() / static_cast<double>(oldWindowSize.x()));
WorldScalar a2(main.window.size.y() / static_cast<double>(oldWindowSize.y()));
return uniformZoom * ((a1 < a2) ? a1 : a2);
}
2025-09-06 19:06:30 +03:00
void World::draw_other_player_cursors(SkCanvas* canvas, const DrawData& drawData) {
if(clients) {
for(auto& clientDataPtr : clients->get_data()) {
if(clientDataPtr != ownClientData)
clientDataPtr->draw_cursor(canvas, drawData);
2025-07-06 20:43:53 +03:00
}
}
}
2025-09-06 19:06:30 +03:00
2025-09-30 16:36:09 +03:00
void World::scale_up_step() {
2025-12-31 16:30:56 +03:00
if(ownClientData)
ownClientData->scale_up_step(ownClientData, *this);
2025-09-30 16:36:09 +03:00
}
void World::scale_up(const WorldScalar& scaleUpAmount) {
2026-05-16 21:56:58 +03:00
Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "Canvas scaled up");
// drawProg will be sending info on committed objects/modified grids. These objects will NOT be scaled up.
// This means that the scale up message must be sent AFTER the World::scale_up function is called on our end
// if this client is the one responsible for the scale up
drawProg.scale_up(scaleUpAmount);
bMan.scale_up(scaleUpAmount);
gridMan.scale_up(scaleUpAmount);
drawData.cam.scale_up(*this, scaleUpAmount);
2026-01-02 17:11:32 +03:00
undo.scale_up(scaleUpAmount);
}
2026-01-18 13:39:49 +03:00
bool World::should_ask_before_closing() {
// If it's a client that didn't save to a file previously, we can ignore asking before quitting (probably don't intend to save anyway)
// If it's a server or a client, and the previous statement is false, we should always ask before quitting because the data could be modified by someone on the network
// If it's a local file, ask before quitting if we detect any local changes
// If on web version, don't ask before closing
2026-02-08 20:54:44 +03:00
#ifdef __EMSCRIPTEN__
2026-01-18 13:39:49 +03:00
return false;
#else
if(netClient || netServer)
return !(netClient && filePath.empty());
return hasUnsavedLocalChanges;
#endif
}
2026-04-05 20:26:03 +03:00
void World::set_has_unsaved_local_changes(bool newHasUnsavedLocalChangesVal) {
bool oldHasUnsavedLocalChanges = hasUnsavedLocalChanges;
hasUnsavedLocalChanges = newHasUnsavedLocalChangesVal;
if(oldHasUnsavedLocalChanges != hasUnsavedLocalChanges)
main.g.gui.set_to_layout();
}
#ifdef ENABLE_ORDERED_LIST_TEST
void World::list_debug_test_update() {
if(std::chrono::steady_clock::now() < (listDebugTestTimeStart + std::chrono::minutes(1))) {
if(nextSendTime < std::chrono::steady_clock::now() - std::chrono::milliseconds(300)) {
nextSendTime = std::chrono::steady_clock::now();
bool isInsert;
if(listDebugTest->size() > 70)
isInsert = false;
else if(listDebugTest->size() < 20)
isInsert = true;
else
isInsert = Random::get().real_range(0.0f, 1.0f) > 0.5f;
using namespace NetworkingObjects;
if(isInsert) {
std::vector<std::pair<NetObjOrderedListIterator<uint16_t>, NetObjOwnerPtr<uint16_t>>> toInsert;
std::vector<uint32_t> randomIndices;
uint32_t insertAmount = Random::get().int_range(5, 25);
for(uint32_t i = 0; i < insertAmount; i++)
randomIndices.emplace_back(Random::get().int_range<uint32_t>(0, listDebugTest->size() + 10));
randomIndices[1] = randomIndices[0];
std::sort(randomIndices.begin(), randomIndices.end());
for(uint32_t index : randomIndices)
toInsert.emplace_back(listDebugTest->at(index), netObjMan.make_obj_direct<uint16_t>(Random::get().int_range<uint32_t>(10, 100)));
listDebugTest->insert_ordered_list_and_send_create(listDebugTest, toInsert);
}
else {
std::vector<NetObjOrderedListIterator<uint16_t>> toErase;
for(uint32_t i = 0; i < listDebugTest->size(); i++) {
if(Random::get().real_range(0.0f, 1.0f) > 0.8f)
toErase.emplace_back(listDebugTest->at(i));
}
listDebugTest->erase_list(listDebugTest, toErase);
}
}
}
if(netServer && netServer->get_client_list().empty())
listDebugTestTimeStart = std::chrono::steady_clock::now();
}
#endif
void World::draw(SkCanvas* canvas, const DrawData& calledDrawData) {
if(!clientStillConnecting) {
if(calledDrawData.drawGrids)
gridMan.draw_back(canvas, calledDrawData);
drawProg.draw(canvas, calledDrawData);
if(calledDrawData.drawGrids) {
gridMan.draw_front(canvas, calledDrawData);
gridMan.draw_coordinates(canvas, calledDrawData);
}
2026-02-25 13:29:46 +03:00
if(!calledDrawData.takingScreenshot)
draw_other_player_cursors(canvas, calledDrawData);
}
2025-09-06 19:06:30 +03:00
}