Some checks are pending
Windows Release / windows-x86_64 (push) Waiting to run
Publish the portable zip to public Forgejo from the private GitHub builder, and force Ninja so MSVC 195 deps build without CMake 4.2.
728 lines
29 KiB
C++
728 lines
29 KiB
C++
/*
|
|
* 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/>.
|
|
*/
|
|
|
|
#include "World.hpp"
|
|
#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>
|
|
#include <cereal/types/unordered_map.hpp>
|
|
#include "DrawingProgram/Layers/DrawingProgramLayerListItem.hpp"
|
|
#include "DrawingProgram/Layers/FogStripper.hpp"
|
|
#include "FingerInputTracker.hpp"
|
|
#include "Helpers/NetworkingObjects/NetObjOrderedList.hpp"
|
|
#include "Helpers/NetworkingObjects/NetObjTemporaryPtr.decl.hpp"
|
|
#include "Helpers/NetworkingObjects/NetObjUnorderedSet.hpp"
|
|
#include "CommandList.hpp"
|
|
#include "Helpers/StringHelpers.hpp"
|
|
#include "MainProgram.hpp"
|
|
#include "SharedTypes.hpp"
|
|
#include "Toolbar.hpp"
|
|
#include "VersionConstants.hpp"
|
|
#include "WorldGrid.hpp"
|
|
#include <cereal/archives/portable_binary.hpp>
|
|
#include <chrono>
|
|
#include <include/core/SkFontMetrics.h>
|
|
#include <Helpers/Networking/NetLibrary.hpp>
|
|
#include <Helpers/Logger.hpp>
|
|
#include <cereal/types/vector.hpp>
|
|
#include <Helpers/Networking/NetLibrary.hpp>
|
|
#include <zstd.h>
|
|
#include "CanvasComponents/CanvasComponent.hpp"
|
|
#include "CanvasComponents/CanvasComponentContainer.hpp"
|
|
#include "CanvasComponents/CanvasComponentAllocator.hpp"
|
|
#include "WorldScreenshot.hpp"
|
|
#include "FileHelpers.hpp"
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
#include <EmscriptenHelpers/emscripten_browser_file.h>
|
|
#endif
|
|
|
|
World::World(MainProgram& initMain, const CustomEvents::OpenInfiniPaintFileEvent& worldInfo):
|
|
netObjMan(!worldInfo.isClient),
|
|
main(initMain),
|
|
undo(*this),
|
|
rMan(*this),
|
|
drawProg(*this),
|
|
bMan(*this),
|
|
gridMan(*this),
|
|
canvasTheme(*this)
|
|
{
|
|
saveThumbnail = worldInfo.saveThumbnail;
|
|
|
|
init_net_obj_type_list();
|
|
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);
|
|
});
|
|
|
|
netSource = worldInfo.netSource;
|
|
|
|
drawData.rMan = &rMan;
|
|
drawData.main = &main;
|
|
drawData.cam.set_viewing_area(main.window.size.cast<float>());
|
|
|
|
if(worldInfo.isClient)
|
|
init_client(worldInfo.netSource);
|
|
else {
|
|
init_client_data_list();
|
|
set_name(name);
|
|
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
|
|
}
|
|
}
|
|
|
|
void World::init_client_data_list() {
|
|
clients = netObjMan.make_obj<NetworkingObjects::NetObjUnorderedSet<ClientData>>();
|
|
serverClientData = ownClientData = clients->emplace_direct(clients, ClientData::InitStruct{
|
|
.cursorColor = get_random_cursor_color(),
|
|
.displayName = main.conf.displayName
|
|
});
|
|
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);
|
|
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);
|
|
Logger::get().log(Logger::LogType::PHONE_USERINFO, objPtr->get_display_name() + " left");
|
|
});
|
|
}
|
|
|
|
void World::init_net_obj_type_list() {
|
|
BookmarkListItem::register_class(*this);
|
|
WorldGrid::register_class(*this);
|
|
NetworkingObjects::register_ordered_list_class<WorldGrid>(netObjMan);
|
|
CanvasComponentAllocator::register_class(*this);
|
|
CanvasComponentContainer::register_class(*this);
|
|
NetworkingObjects::register_ordered_list_class<CanvasComponentContainer>(netObjMan);
|
|
DrawingProgramLayerListItem::register_class(*this);
|
|
NetworkingObjects::register_ordered_list_class<DrawingProgramLayerListItem>(netObjMan);
|
|
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
|
|
}
|
|
|
|
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);
|
|
netObjMan.set_client(netClient, SERVER_UPDATE_NETWORK_OBJECT);
|
|
set_name("");
|
|
|
|
rMan.init_client_callbacks();
|
|
drawProg.init_client_callbacks();
|
|
netClient->add_recv_callback(CLIENT_INITIAL_DATA, [&](cereal::PortableBinaryInputArchive& message) {
|
|
std::string fileDisplayName;
|
|
NetworkingObjects::NetObjID clientDataObjID, serverDataObjID;
|
|
message(fileDisplayName, clientDataObjID, serverDataObjID);
|
|
set_name(fileDisplayName);
|
|
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);
|
|
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
|
|
|
|
clientStillConnecting = false;
|
|
set_to_layout_gui_if_focus();
|
|
});
|
|
netClient->add_recv_callback(CLIENT_UPDATE_NETWORK_OBJECT, [&](cereal::PortableBinaryInputArchive& message) {
|
|
netObjMan.read_update_message(message, nullptr);
|
|
});
|
|
netClient->add_recv_callback(CLIENT_KEEP_ALIVE, [&](cereal::PortableBinaryInputArchive& message) {
|
|
});
|
|
|
|
netClient->send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_INITIAL_DATA, main.conf.displayName);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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);
|
|
drawProg.update();
|
|
#ifdef ENABLE_ORDERED_LIST_TEST
|
|
list_debug_test_update();
|
|
#endif
|
|
}
|
|
|
|
drawData.cam.update_main(*this);
|
|
|
|
rMan.update();
|
|
}
|
|
|
|
bool World::connection_update() {
|
|
if(netServer) {
|
|
if(netServer->is_disconnected()) {
|
|
Logger::get().log(Logger::LogType::USERINFO, "Host connection failed");
|
|
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()) {
|
|
Logger::get().log(Logger::LogType::USERINFO, "Client connection failed");
|
|
netObjMan.disconnect();
|
|
main.set_tab_to_close(this);
|
|
netClient = nullptr;
|
|
return false;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool World::is_focus() {
|
|
return main.world.get() == this;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void World::update() {
|
|
connection_update();
|
|
}
|
|
|
|
void World::on_tab_out() {
|
|
rMan.clear_display_cache();
|
|
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);
|
|
drawData.cam.input_key_callback(key);
|
|
}
|
|
}
|
|
|
|
void World::input_mouse_button_callback(const InputManager::MouseButtonCallbackArgs& button) {
|
|
if(!clientStillConnecting) {
|
|
drawProg.input_mouse_button_callback(button);
|
|
drawData.cam.input_mouse_button_on_canvas_callback(*this, button);
|
|
}
|
|
}
|
|
|
|
void World::input_mouse_motion_callback(const InputManager::MouseMotionCallbackArgs& motion) {
|
|
if(!clientStillConnecting) {
|
|
drawProg.input_mouse_motion_callback(motion);
|
|
drawData.cam.input_mouse_motion_callback(*this, motion);
|
|
}
|
|
}
|
|
|
|
void World::input_mouse_wheel_callback(const InputManager::MouseWheelCallbackArgs& wheel) {
|
|
if(!clientStillConnecting)
|
|
drawData.cam.input_mouse_wheel_callback(*this, wheel);
|
|
}
|
|
|
|
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_finger_touch_callback(const FingerInput::TouchCallbackArgs& touch) {
|
|
if(!clientStillConnecting)
|
|
drawData.cam.input_finger_touch_callback(*this, touch);
|
|
}
|
|
|
|
std::optional<InputManager::TextBoxStartInfo> World::get_text_box_start_info() {
|
|
if(!clientStillConnecting)
|
|
return drawProg.get_text_box_start_info();
|
|
return std::nullopt;
|
|
}
|
|
|
|
void World::send_chat_message(const std::string& message) {
|
|
if(!clientStillConnecting)
|
|
ownClientData->send_chat_message(ownClientData, *this, message);
|
|
}
|
|
|
|
void World::add_chat_message(const std::string& name, const std::string& message, Toolbar::ChatMessage::Type type) {
|
|
Logger::get().log(Logger::LogType::CHAT, type == Toolbar::ChatMessage::JOIN ? (name + " " + message) : ("[" + name + "] " + message));
|
|
chatMessages.emplace_front(Toolbar::ChatMessage{name, message, type});
|
|
if(chatMessages.size() == CHAT_SIZE)
|
|
chatMessages.pop_back();
|
|
set_to_layout_gui_if_focus();
|
|
}
|
|
|
|
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) {
|
|
std::vector<std::string> strList;
|
|
for(auto& client : clients->get_data())
|
|
strList.emplace_back(client->get_display_name());
|
|
displayName = ensure_string_unique(strList, displayName);
|
|
}
|
|
|
|
void World::start_hosting(const std::string& initNetSource, const std::string& serverLocalID) {
|
|
main.init_net_library();
|
|
netServer = std::make_shared<NetServer>(serverLocalID);
|
|
lastKeepAliveSent = std::chrono::steady_clock::now();
|
|
NetLibrary::register_server(netServer);
|
|
netObjMan.set_server(netServer, CLIENT_UPDATE_NETWORK_OBJECT);
|
|
netSource = initNetSource;
|
|
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);
|
|
a(CLIENT_INITIAL_DATA, name, clientDataObjPtr.get_net_id(), serverClientData.get_net_id());
|
|
clients.write_create_message(a);
|
|
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
|
|
}
|
|
netServer->send_string_stream_to_client(client, RELIABLE_COMMAND_CHANNEL, ss);
|
|
auto visibleResources = FogStripper::compute_visible_resource_ids(drawProg.layerMan, drawProg.layerMan.clientHiddenComponentIds);
|
|
for(auto& r : rMan.resource_list()) {
|
|
if(!visibleResources.contains(r.get_net_id()))
|
|
continue;
|
|
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) {
|
|
currentNetUpdateClient = client;
|
|
netObjMan.read_update_message(message, client);
|
|
currentNetUpdateClient = nullptr;
|
|
drawProg.layerMan.flush_deferred_rejects();
|
|
});
|
|
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);
|
|
});
|
|
}
|
|
|
|
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(...) {
|
|
}
|
|
std::string nameToSaveUnder = ensure_string_unique(strList, name);
|
|
save_to_file(directoryToSaveAt / std::filesystem::path(nameToSaveUnder + "." + FILE_EXTENSION));
|
|
}
|
|
|
|
void World::save_to_file(const std::filesystem::path& filePathToSaveAt) {
|
|
try {
|
|
if(main.conf.forceExtensionOnPath)
|
|
filePath = force_extension_on_path(filePathToSaveAt, FILE_EXTENSION);
|
|
else
|
|
filePath = filePathToSaveAt;
|
|
|
|
std::stringstream f;
|
|
f.write(VersionConstants::CURRENT_SAVEFILE_HEADER.c_str(), VersionConstants::SAVEFILE_HEADER_LEN);
|
|
|
|
{
|
|
std::stringstream fWorldDataToCompress;
|
|
{
|
|
cereal::PortableBinaryOutputArchive a(fWorldDataToCompress);
|
|
save_file(a);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
set_name(filePath.stem().string());
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
emscripten_browser_file::download(
|
|
filePath.string(),
|
|
"application/octet-stream",
|
|
f.view()
|
|
);
|
|
#else
|
|
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
|
|
});
|
|
}
|
|
#endif
|
|
|
|
Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "File saved");
|
|
undo.set_save_action();
|
|
}
|
|
catch(const std::exception& e) {
|
|
Logger::get().log(Logger::LogType::WORLDFATAL, std::string("Save error: ") + e.what());
|
|
}
|
|
}
|
|
|
|
void World::load_empty_canvas(const std::optional<std::filesystem::path>& filePathEmptyAutoSaveDir) {
|
|
gridMan.server_init_no_file();
|
|
bMan.server_init_no_file();
|
|
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);
|
|
drawProg.server_init_no_file();
|
|
canvasTheme.server_init_no_file();
|
|
|
|
if(filePathEmptyAutoSaveDir.has_value()) {
|
|
bool oldSaveThumbnail = true;
|
|
saveThumbnail = false;
|
|
autosave_to_directory(filePathEmptyAutoSaveDir.value());
|
|
saveThumbnail = oldSaveThumbnail;
|
|
}
|
|
}
|
|
|
|
void World::load_from_file(const std::filesystem::path& filePathToLoadFrom, std::string_view buffer) {
|
|
filePath = filePathToLoadFrom;
|
|
|
|
std::string byteDataFromFile;
|
|
|
|
if(buffer.empty()) {
|
|
byteDataFromFile = read_file_to_string(filePath);
|
|
buffer = byteDataFromFile;
|
|
}
|
|
|
|
if(buffer.size() < VersionConstants::SAVEFILE_HEADER_LEN)
|
|
throw std::runtime_error("[World::load_from_file] File is not an InfiniPaint canvas (File too small)");
|
|
|
|
std::string_view fileHeader = buffer.substr(0, VersionConstants::SAVEFILE_HEADER_LEN);
|
|
VersionNumber fileVersion = VersionConstants::header_to_version_number(std::string(fileHeader));
|
|
std::string_view uncompressedDataView;
|
|
std::vector<char> uncompressedDataVector;
|
|
|
|
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);
|
|
uncompressedDataView = std::string_view(uncompressedDataVector.data(), trueUncompressedSize);
|
|
}
|
|
|
|
ByteMemStream f((char*)uncompressedDataView.data(), uncompressedDataView.size());
|
|
|
|
Logger::get().log(Logger::LogType::INFO, "Loading file from version " + version_numbers_to_version_str(fileVersion));
|
|
|
|
cereal::PortableBinaryInputArchive a(f);
|
|
load_file(a, fileVersion);
|
|
|
|
Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "File loaded");
|
|
set_name(filePath.stem().string());
|
|
}
|
|
|
|
void World::save_file(cereal::PortableBinaryOutputArchive& a) const {
|
|
drawData.cam.save_file(a, *this);
|
|
canvasTheme.save_file(a);
|
|
drawProg.save_file(a);
|
|
bMan.save_file(a);
|
|
gridMan.save_file(a);
|
|
rMan.save_file(a);
|
|
}
|
|
|
|
void World::load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version) {
|
|
drawData.cam.load_file(a, version, *this);
|
|
canvasTheme.load_file(a, version);
|
|
drawProg.load_file(a, version);
|
|
bMan.load_file(a, version);
|
|
gridMan.load_file(a, version);
|
|
rMan.load_file(a, version);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
void World::scale_up_step() {
|
|
if(ownClientData)
|
|
ownClientData->scale_up_step(ownClientData, *this);
|
|
}
|
|
|
|
void World::scale_up(const WorldScalar& scaleUpAmount) {
|
|
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);
|
|
undo.scale_up(scaleUpAmount);
|
|
}
|
|
|
|
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
|
|
#ifdef __EMSCRIPTEN__
|
|
return false;
|
|
#else
|
|
if(netClient || netServer)
|
|
return !(netClient && filePath.empty());
|
|
return hasUnsavedLocalChanges;
|
|
#endif
|
|
}
|
|
|
|
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);
|
|
}
|
|
if(!calledDrawData.takingScreenshot)
|
|
draw_other_player_cursors(canvas, calledDrawData);
|
|
}
|
|
}
|