diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a79194..691cd56 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ set(sources "src/main.cpp" "src/DrawData.cpp" "src/FontData.cpp" "src/ClientData.cpp" + "src/CollabPermissions.cpp" "src/GridManager.cpp" "src/WorldGrid.cpp" "src/TimePoint.cpp" diff --git a/FORK.md b/FORK.md index d907a46..4e07232 100644 --- a/FORK.md +++ b/FORK.md @@ -4,7 +4,7 @@ Upstream: [ErrorAtLine0/infinipaint](https://github.com/ErrorAtLine0/infinipaint **Public forge:** https://forgejo.fsociety00.cc/copilot/infinipaint GitHub (`littlecodedragon/infinipaint`) is **private builder only** — not a public fork mirror. Source of truth stays on Forgejo. -Save format: `INFPNT000008` / version `0.8.0-fork` (not network-compatible with stock 0.6.x peers). +Save format: `INFPNT000009` / version `0.9.0-fork` (not network-compatible with stock 0.6.x peers). ## CI @@ -33,4 +33,18 @@ Not automatic on import. Choose **Paste Image + Freistellen** from the canvas ri ## Fog of War / layer lock (host) -In the layer panel (host / local file only): mark a leaf layer as **Fog of War layer** (must sit **above** the content it covers), then paint fog with brush/shapes on that layer. Players receive a **stripped** world — only objects **fully** covered by opaque fog (not merely overlapping) are omitted from join sync and from their save. Partially covered images stay. Moving or reshaping fog (or content under it) live-updates clients: newly covered objects are unloaded, revealed ones are sent again with their resources. The host file stays complete. Fog draws at the layer’s real alpha (1.0 = fully opaque for host and players). **Lock for players** blocks client edits on a normal layer; fog layers are always locked for players (move/edit/erase rejected on the host). Players also **cannot hide or fade** a fog layer (eye / alpha); only the host can toggle fog visibility, and the host rejects client DisplayData that would strip FoW. +In the layer panel (host / local file only): mark a leaf layer as **Fog of War layer** (must sit **above** the content it covers), then paint fog with brush/shapes on that layer. Players receive a **stripped** world — only objects **fully** covered by opaque fog (not merely overlapping) are omitted from join sync and from their save. Partially covered images stay. Moving or reshaping fog (or content under it) live-updates clients: newly covered objects are unloaded, revealed ones are sent again with their resources. The host file stays complete. Fog draws at the layer’s real alpha (1.0 = fully opaque for host and players). **Lock for players** is a quick toggle that sets layer-default Draw/Move/Delete to Deny; fog layers are always locked for players (move/edit/erase rejected on the host). Players also **cannot hide or fade** a fog layer (eye / alpha); only the host can toggle fog visibility, and the host rejects client DisplayData that would strip FoW. FoW visibility is still global (not per-person). + +## Collab permissions (host) + +Rights: **Draw**, **Move**, **Delete**, **Hide**, **Layers** (create/reorder/delete layers). Each right is Allow / Deny / Inherit. + +Resolution (first concrete wins): Person×Layer override → Layer defaults → Person defaults → World default (Inherit at world = Allow). + +Host UI: + +- Player list → **Session Defaults…** (world / new-player / new-layer templates) +- Player list → **Rights** per connected player (person defaults; copied from new-player template on join) +- Layer panel (host): layer defaults + per-player overrides for the selected layer + +Clients cannot escalate their own ACL; host DisplayData/ClientData updates are sanitized. diff --git a/src/CanvasComponents/CanvasComponentAllocator.cpp b/src/CanvasComponents/CanvasComponentAllocator.cpp index 059b233..89dcaac 100644 --- a/src/CanvasComponents/CanvasComponentAllocator.cpp +++ b/src/CanvasComponents/CanvasComponentAllocator.cpp @@ -45,10 +45,10 @@ void CanvasComponentAllocator::register_class(World& world) { .writeUpdate = [](const CanvasComponentAllocator& o, cereal::PortableBinaryOutputArchive& a) { a(*o.comp); }, - .readUpdate = [](CanvasComponentAllocator& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr& c) { - // Host rejects client edits of fog/locked-layer components (still consume the payload). - if(c && o.comp && o.comp->compContainer && o.comp->compContainer->parentLayer - && o.comp->compContainer->parentLayer->is_player_edit_blocked()) { + .readUpdate = [&world](CanvasComponentAllocator& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr& c) { + // Host rejects client edits without Draw on that layer (still consume the payload). + if(c && o.comp && o.comp->compContainer + && !world.drawProg.layerMan.client_may_modify_component(o.comp->compContainer, CollabRight::Draw)) { auto junk = std::unique_ptr(CanvasComponent::allocate_comp(o.comp->get_type())); a(*junk); return; diff --git a/src/ClientData.cpp b/src/ClientData.cpp index 76442e1..8c5ad4b 100644 --- a/src/ClientData.cpp +++ b/src/ClientData.cpp @@ -36,7 +36,8 @@ enum class ClientDataCommand : uint8_t { SET_WINDOW_SIZE, SET_CAM_COORDS, SEND_CHAT_MESSAGE, - SET_GRID_SIZE + SET_GRID_SIZE, + SET_PERSON_DEFAULTS }; ClientData::ClientData() { @@ -54,6 +55,7 @@ void ClientData::set_from_init_struct(const InitStruct& initStruct) { cursorColor = initStruct.cursorColor; displayName = initStruct.displayName; gridSize = initStruct.gridSize; + personDefaults = initStruct.personDefaults; } void ClientData::register_class(World& world) { @@ -92,6 +94,11 @@ void ClientData::register_class(World& world) { } break; } + case ClientDataCommand::SET_PERSON_DEFAULTS: + a(o->personDefaults); + world.drawProg.layerMan.ensure_editing_layer_is_editable(); + world.set_to_layout_gui_if_focus(); + break; } }, .writeConstructorFuncServer = default_serialize_write_func, @@ -141,6 +148,20 @@ void ClientData::register_class(World& world) { }); break; } + case ClientDataCommand::SET_PERSON_DEFAULTS: { + // Only the host may change ACLs — junk-consume client attempts. + CollabPermissionSet incoming; + a(incoming); + if(c) + break; + o->personDefaults = incoming; + o.send_server_update_to_all_clients(RELIABLE_COMMAND_CHANNEL, [](const NetObjTemporaryPtr& o, cereal::PortableBinaryOutputArchive & a) { + a(ClientDataCommand::SET_PERSON_DEFAULTS, o->personDefaults); + }); + world.drawProg.layerMan.ensure_editing_layer_is_editable(); + world.set_to_layout_gui_if_focus(); + break; + } } }, }); @@ -187,6 +208,15 @@ void ClientData::scale_up_step(const NetworkingObjects::NetObjTemporaryPtr& o, const CollabPermissionSet& defaults) { + if(o->personDefaults == defaults) + return; + o->personDefaults = defaults; + o.send_update_to_all(RELIABLE_COMMAND_CHANNEL, [](const NetObjTemporaryPtr& o, cereal::PortableBinaryOutputArchive & a) { + a(ClientDataCommand::SET_PERSON_DEFAULTS, o->personDefaults); + }); +} + void ClientData::set_display_name(const std::string& newDisplayName) { displayName = newDisplayName; } @@ -219,6 +249,14 @@ const Vector3f& ClientData::get_cursor_color() const { return cursorColor; } +const CollabPermissionSet& ClientData::get_person_defaults() const { + return personDefaults; +} + +CollabPermissionSet& ClientData::person_defaults_mut() { + return personDefaults; +} + void ClientData::draw_cursor(SkCanvas* canvas, const DrawData& drawData) const { if((drawData.cam.c.inverseScale << 10) > camCoords.inverseScale && drawData.clampDrawMinimum < camCoords.inverseScale) { canvas->save(); diff --git a/src/ClientData.hpp b/src/ClientData.hpp index 3a4f1da..01aacac 100644 --- a/src/ClientData.hpp +++ b/src/ClientData.hpp @@ -19,6 +19,7 @@ #pragma once #include #include "CoordSpaceHelper.hpp" +#include "CollabPermissions.hpp" #include #include @@ -32,6 +33,7 @@ class ClientData { Vector3f cursorColor = {1.0f, 1.0f, 1.0f}; std::string displayName; uint32_t gridSize = 0; + CollabPermissionSet personDefaults = CollabPermissionSet::all_inherit(); }; ClientData(); @@ -42,6 +44,8 @@ class ClientData { static void set_camera_coords(const NetworkingObjects::NetObjTemporaryPtr& o, const CoordSpaceHelper& newCoords); static void send_chat_message(const NetworkingObjects::NetObjTemporaryPtr& o, World& world, const std::string& chatMessage); static void scale_up_step(const NetworkingObjects::NetObjTemporaryPtr& o, World& world); + /** Host-only: push person-default ACL to all peers. */ + static void set_person_defaults(const NetworkingObjects::NetObjTemporaryPtr& o, const CollabPermissionSet& defaults); void set_display_name(const std::string& newDisplayName); void set_cursor_color(const Vector3f& newCursorColor); @@ -51,11 +55,13 @@ class ClientData { uint32_t get_grid_size() const; const std::string& get_display_name() const; const Vector3f& get_cursor_color() const; + const CollabPermissionSet& get_person_defaults() const; + CollabPermissionSet& person_defaults_mut(); void draw_cursor(SkCanvas* canvas, const DrawData& drawData) const; template void serialize(Archive& a) { - a(camCoords, windowSize, cursorPos, cursorColor, displayName, gridSize); + a(camCoords, windowSize, cursorPos, cursorColor, displayName, gridSize, personDefaults); } private: void set_from_init_struct(const InitStruct& initStruct); @@ -66,5 +72,6 @@ class ClientData { Vector3f cursorColor; std::string displayName; uint32_t gridSize; + CollabPermissionSet personDefaults; }; diff --git a/src/CollabPermissions.cpp b/src/CollabPermissions.cpp new file mode 100644 index 0000000..b2963c4 --- /dev/null +++ b/src/CollabPermissions.cpp @@ -0,0 +1,114 @@ +#include "CollabPermissions.hpp" + +CollabTriState CollabPermissionSet::get(CollabRight right) const { + switch(right) { + case CollabRight::Draw: return draw; + case CollabRight::Move: return move; + case CollabRight::Delete: return erase; + case CollabRight::Hide: return hide; + case CollabRight::ManageLayers: return manageLayers; + case CollabRight::COUNT: break; + } + return CollabTriState::Inherit; +} + +void CollabPermissionSet::set(CollabRight right, CollabTriState state) { + switch(right) { + case CollabRight::Draw: draw = state; break; + case CollabRight::Move: move = state; break; + case CollabRight::Delete: erase = state; break; + case CollabRight::Hide: hide = state; break; + case CollabRight::ManageLayers: manageLayers = state; break; + case CollabRight::COUNT: break; + } +} + +bool CollabPermissionSet::any_concrete() const { + return draw != CollabTriState::Inherit + || move != CollabTriState::Inherit + || erase != CollabTriState::Inherit + || hide != CollabTriState::Inherit + || manageLayers != CollabTriState::Inherit; +} + +CollabPermissionSet CollabPermissionSet::all_allow() { + CollabPermissionSet s; + s.draw = CollabTriState::Allow; + s.move = CollabTriState::Allow; + s.erase = CollabTriState::Allow; + s.hide = CollabTriState::Allow; + s.manageLayers = CollabTriState::Allow; + return s; +} + +CollabPermissionSet CollabPermissionSet::all_inherit() { + return CollabPermissionSet{}; +} + +CollabPermissionSet CollabPermissionSet::content_deny() { + CollabPermissionSet s; + s.draw = CollabTriState::Deny; + s.move = CollabTriState::Deny; + s.erase = CollabTriState::Deny; + return s; +} + +bool collab_resolve_allow( + CollabRight right, + const CollabPermissionSet* personLayerOverride, + const CollabPermissionSet& layerDefaults, + const CollabPermissionSet& personDefaults, + const CollabPermissionSet& worldDefault) +{ + auto pick = [&](const CollabPermissionSet& set) -> CollabTriState { + return set.get(right); + }; + if(personLayerOverride) { + CollabTriState s = pick(*personLayerOverride); + if(s != CollabTriState::Inherit) + return s == CollabTriState::Allow; + } + { + CollabTriState s = pick(layerDefaults); + if(s != CollabTriState::Inherit) + return s == CollabTriState::Allow; + } + { + CollabTriState s = pick(personDefaults); + if(s != CollabTriState::Inherit) + return s == CollabTriState::Allow; + } + CollabTriState s = pick(worldDefault); + // World Inherit → Allow (open by default). + return s != CollabTriState::Deny; +} + +const char* collab_right_label(CollabRight right) { + switch(right) { + case CollabRight::Draw: return "Draw"; + case CollabRight::Move: return "Move"; + case CollabRight::Delete: return "Delete"; + case CollabRight::Hide: return "Hide"; + case CollabRight::ManageLayers: return "Layers"; + case CollabRight::COUNT: break; + } + return "?"; +} + +const char* collab_tristate_label(CollabTriState state) { + switch(state) { + case CollabTriState::Inherit: return "Inherit"; + case CollabTriState::Allow: return "Allow"; + case CollabTriState::Deny: return "Deny"; + } + return "?"; +} + +CollabTriState collab_cycle_tristate(CollabTriState state) { + switch(state) { + case CollabTriState::Inherit: return CollabTriState::Allow; + case CollabTriState::Allow: return CollabTriState::Deny; + case CollabTriState::Deny: return CollabTriState::Inherit; + } + return CollabTriState::Inherit; +} diff --git a/src/CollabPermissions.hpp b/src/CollabPermissions.hpp new file mode 100644 index 0000000..5f5e0c5 --- /dev/null +++ b/src/CollabPermissions.hpp @@ -0,0 +1,77 @@ +/* + * InfiniPaint (fork) + * Collab ACL: World → Person → Layer → Person×Layer (Allow/Deny/Inherit) + */ +#pragma once + +#include +#include +#include +#include +#include + +enum class CollabRight : uint8_t { + Draw = 0, + Move, + Delete, + Hide, + ManageLayers, + COUNT +}; + +enum class CollabTriState : uint8_t { + Inherit = 0, + Allow = 1, + Deny = 2 +}; + +struct CollabPermissionSet { + CollabTriState draw = CollabTriState::Inherit; + CollabTriState move = CollabTriState::Inherit; + CollabTriState erase = CollabTriState::Inherit; + CollabTriState hide = CollabTriState::Inherit; + CollabTriState manageLayers = CollabTriState::Inherit; + + CollabTriState get(CollabRight right) const; + void set(CollabRight right, CollabTriState state); + bool any_concrete() const; + + static CollabPermissionSet all_allow(); + static CollabPermissionSet all_inherit(); + static CollabPermissionSet content_deny(); // Draw/Move/Delete Deny, rest Inherit + + template + void serialize(Archive& a) { + a(draw, move, erase, hide, manageLayers); + } + + bool operator==(const CollabPermissionSet&) const = default; +}; + +struct CollabSessionDefaults { + // Concrete fallback when everything inherits (Inherit treated as Allow). + CollabPermissionSet worldDefault = CollabPermissionSet::all_allow(); + // Copied onto ClientData when a player joins. + CollabPermissionSet newPlayerDefaults = CollabPermissionSet::all_inherit(); + // Copied onto new layers' DisplayData.layerDefaults. + CollabPermissionSet newLayerDefaults = CollabPermissionSet::all_inherit(); + + template + void serialize(Archive& a) { + a(worldDefault, newPlayerDefaults, newLayerDefaults); + } + + bool operator==(const CollabSessionDefaults&) const = default; +}; + +/** Resolve Allow/Deny for peer on a layer. Host callers should short-circuit before this. */ +bool collab_resolve_allow( + CollabRight right, + const CollabPermissionSet* personLayerOverride, // nullable + const CollabPermissionSet& layerDefaults, + const CollabPermissionSet& personDefaults, + const CollabPermissionSet& worldDefault); + +const char* collab_right_label(CollabRight right); +const char* collab_tristate_label(CollabTriState state); +CollabTriState collab_cycle_tristate(CollabTriState state); diff --git a/src/DrawingProgram/DrawingProgram.cpp b/src/DrawingProgram/DrawingProgram.cpp index 97fa77b..485b2fb 100644 --- a/src/DrawingProgram/DrawingProgram.cpp +++ b/src/DrawingProgram/DrawingProgram.cpp @@ -48,6 +48,8 @@ #include #include "../CanvasComponents/ImageCanvasComponent.hpp" #include "Layers/DrawingProgramLayer.hpp" +#include "../CollabPermissions.hpp" +#include #include "../ImagePreprocess.hpp" #include #include @@ -308,7 +310,44 @@ std::optional DrawingProgram::get_text_box_start return drawTool->get_text_box_start_info(); } +void DrawingProgram::register_collab_defaults_class(World& world) { + world.delayedUpdateObjectManager.register_class(world.netObjMan, NetworkingObjects::DelayUpdateSerializedClassManager::CustomConstructors{ + .readUpdate = [](CollabSessionDefaults& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr& c) { + CollabSessionDefaults incoming; + a(incoming); + // Clients cannot escalate session defaults. + if(c) + return; + o = incoming; + }, + .postUpdateFunc = [&world](CollabSessionDefaults&) { + world.drawProg.layerMan.ensure_editing_layer_is_editable(); + world.set_to_layout_gui_if_focus(); + } + }); +} + +void DrawingProgram::ensure_collab_defaults() { + if(!collabDefaults) + collabDefaults = world.netObjMan.make_obj(); +} + +const CollabSessionDefaults& DrawingProgram::collab_defaults() const { + static const CollabSessionDefaults kFallback{}; + return collabDefaults ? *collabDefaults : kFallback; +} + +void DrawingProgram::set_collab_defaults(const CollabSessionDefaults& defaults) { + ensure_collab_defaults(); + if(*collabDefaults == defaults) + return; + *collabDefaults = defaults; + world.delayedUpdateObjectManager.send_update_to_all(collabDefaults, false); + layerMan.ensure_editing_layer_is_editable(); +} + void DrawingProgram::server_init_no_file() { + ensure_collab_defaults(); layerMan.server_init_no_file(); } @@ -322,10 +361,13 @@ void DrawingProgram::scale_up(const WorldScalar& scaleUpAmount) { } void DrawingProgram::write_components_server(cereal::PortableBinaryOutputArchive& a) { + ensure_collab_defaults(); + collabDefaults.write_create_message(a); layerMan.write_components_server(a); } void DrawingProgram::read_components_client(cereal::PortableBinaryInputArchive& a) { + collabDefaults = world.netObjMan.read_create_message(a, nullptr); layerMan.read_components_client(a); drawCache.build({}); } @@ -335,6 +377,7 @@ void DrawingProgram::init_server_callbacks() { std::vector> transforms; message(transforms); + world.currentNetUpdateClient = client; auto clientData = world.netObjMan.get_obj_temporary_ref_from_id(NetworkingObjects::NetObjID(client->customID)); if(clientData->get_grid_size() < world.ownClientData->get_grid_size()) { WorldScalar scaleUpAmount = get_canvas_scale_up_amount(world.ownClientData->get_grid_size(), clientData->get_grid_size()); @@ -349,12 +392,13 @@ void DrawingProgram::init_server_callbacks() { auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id(netID); if(!objPtr) continue; - if(!layerMan.client_may_modify_component(objPtr.get())) { + if(!layerMan.client_may_modify_component(objPtr.get(), CollabRight::Move)) { rejectedRestore.emplace_back(netID, objPtr->coords); continue; } allowed.emplace_back(netID, coord); } + world.currentNetUpdateClient = nullptr; if(!allowed.empty()) { process_transform_message(allowed); world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, allowed); @@ -377,15 +421,16 @@ void DrawingProgram::init_server_callbacks() { world.netServer->add_recv_callback(SERVER_SET_COMPONENT_GROUP_IDS, [&](std::shared_ptr client, cereal::PortableBinaryInputArchive& message) { std::vector> groupIds; message(groupIds); - (void)client; + world.currentNetUpdateClient = client; std::vector> allowed; allowed.reserve(groupIds.size()); for(auto& [netID, groupId] : groupIds) { auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id(netID); - if(!objPtr || !layerMan.client_may_modify_component(objPtr.get())) + if(!objPtr || !layerMan.client_may_modify_component(objPtr.get(), CollabRight::Move)) continue; allowed.emplace_back(netID, groupId); } + world.currentNetUpdateClient = nullptr; if(allowed.empty()) return; process_group_id_message(allowed); @@ -466,7 +511,7 @@ void DrawingProgram::send_transforms_for(const std::vectorobj) continue; - if(!world.netObjMan.is_server() && !layerMan.client_may_modify_component(obj->obj.get())) + if(!world.netObjMan.is_server() && !layerMan.own_peer_may(CollabRight::Move, obj->obj->parentLayer)) continue; transforms.emplace_back(obj->obj.get_net_id(), obj->obj->coords); } @@ -1166,11 +1211,16 @@ void DrawingProgram::draw_drag_circle(SkCanvas* canvas, const Vector2f& sPos, co void DrawingProgram::load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version) { if(version >= VersionNumber(0, 6, 0)) a(controls.lockedCameraScale); + ensure_collab_defaults(); + if(version >= VersionNumber(0, 9, 0)) + a(*collabDefaults); layerMan.load_file(a, version); } void DrawingProgram::save_file(cereal::PortableBinaryOutputArchive& a) const { a(controls.lockedCameraScale); + const_cast(this)->ensure_collab_defaults(); + a(*collabDefaults); layerMan.save_file(a); } diff --git a/src/DrawingProgram/DrawingProgram.hpp b/src/DrawingProgram/DrawingProgram.hpp index 9138a4b..4285ca7 100644 --- a/src/DrawingProgram/DrawingProgram.hpp +++ b/src/DrawingProgram/DrawingProgram.hpp @@ -37,6 +37,7 @@ #include #include "Layers/DrawingProgramLayerManager.hpp" #include "DrawingProgramSelection.hpp" +#include "../CollabPermissions.hpp" class World; class PhoneDrawingProgramScreen; @@ -71,6 +72,12 @@ class DrawingProgram { DrawingProgramCache drawCache; DrawingProgramLayerManager layerMan; + /** Host-owned session ACL defaults (world / new player / new layer). */ + NetworkingObjects::NetObjOwnerPtr collabDefaults; + void ensure_collab_defaults(); + const CollabSessionDefaults& collab_defaults() const; + void set_collab_defaults(const CollabSessionDefaults& defaults); + static void register_collab_defaults_class(World& world); Vector4f* get_foreground_color_ptr(); void switch_to_tool(DrawingProgramToolType newToolType, bool force = false); diff --git a/src/DrawingProgram/Layers/DrawingProgramLayer.cpp b/src/DrawingProgram/Layers/DrawingProgramLayer.cpp index 994de1f..7ef51c3 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayer.cpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayer.cpp @@ -40,9 +40,10 @@ void DrawingProgramLayer::set_component_list_callbacks(DrawingProgramLayerListIt if(c->obj->get_comp().get_type() == CanvasComponentType::IMAGE) layerMan.drawP.updateableComponents.emplace(&(*c)); - // Live fog / lock enforcement on host + // Live ACL / fog enforcement on host if(layerMan.addToCacheOnComponentInsert && layerMan.drawP.world.netServer) { - if(layerMan.drawP.world.currentNetUpdateClient && layerListItem.is_player_edit_blocked()) { + if(layerMan.drawP.world.currentNetUpdateClient + && !layerMan.current_net_client_may(CollabRight::Draw, &layerListItem)) { layerMan.defer_reject_component(&layerListItem, c->obj.get_net_id()); return; } diff --git a/src/DrawingProgram/Layers/DrawingProgramLayerListItem.cpp b/src/DrawingProgram/Layers/DrawingProgramLayerListItem.cpp index 6c5d419..a849c61 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayerListItem.cpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayerListItem.cpp @@ -19,11 +19,13 @@ #include "DrawingProgramLayerListItem.hpp" #include "DrawingProgramLayerFolder.hpp" #include +#include #include "DrawingProgramLayer.hpp" #include "FogStripper.hpp" #include "../../World.hpp" #include "../../MainProgram.hpp" #include "SerializedBlendMode.hpp" +#include "../../DrawingProgram/DrawingProgram.hpp" using namespace NetworkingObjects; @@ -179,17 +181,39 @@ DrawingProgramLayer& DrawingProgramLayerListItem::get_layer() const { return *layerData; } +void DrawingProgramLayerListItem::DisplayData::migrate_player_locked_into_defaults() { + if(playerLocked && !fogLayer) + layerDefaults = CollabPermissionSet::content_deny(); +} + +void DrawingProgramLayerListItem::DisplayData::sync_player_locked_from_defaults() { + if(fogLayer) { + playerLocked = true; + return; + } + playerLocked = layerDefaults.draw == CollabTriState::Deny + && layerDefaults.move == CollabTriState::Deny + && layerDefaults.erase == CollabTriState::Deny; +} + void DrawingProgramLayerListItem::load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version, DrawingProgramLayerManager& layerMan) { nameData = layerMan.drawP.world.netObjMan.make_obj(); a(*nameData); displayData = layerMan.drawP.world.netObjMan.make_obj(); - if(version >= VersionNumber(0, 8, 0)) { + if(version >= VersionNumber(0, 9, 0)) { a(*displayData); - // Normalize cereal bools (non-0/1 bytes must not stick as "true" forever). displayData->visible = displayData->visible ? true : false; displayData->playerLocked = displayData->playerLocked ? true : false; displayData->fogLayer = displayData->fogLayer ? true : false; + displayData->sync_player_locked_from_defaults(); + } + else if(version >= VersionNumber(0, 8, 0)) { + a(displayData->alpha, displayData->visible, displayData->blendMode, displayData->playerLocked, displayData->fogLayer); + displayData->visible = displayData->visible ? true : false; + displayData->playerLocked = displayData->playerLocked ? true : false; + displayData->fogLayer = displayData->fogLayer ? true : false; + displayData->migrate_player_locked_into_defaults(); } else { a(displayData->alpha, displayData->visible, displayData->blendMode); @@ -262,6 +286,8 @@ void DrawingProgramLayerListItem::set_alpha(DrawingProgramLayerManager& layerMan // Players must not fade fog out — same authority as visibility. if(is_fog_layer() && !FogStripper::world_is_host(layerMan.drawP.world)) return; + if(!FogStripper::world_is_host(layerMan.drawP.world) && !layerMan.own_peer_may(CollabRight::Hide, this)) + return; if(displayData && displayData->alpha != newAlpha) { displayData->alpha = newAlpha; layerMan.drawP.drawCache.clear_own_cached_surfaces(); @@ -277,6 +303,8 @@ void DrawingProgramLayerListItem::set_visible(DrawingProgramLayerManager& layerM // Fog of War is host-controlled; clients hiding it would sync and strip FoW for everyone. if(is_fog_layer() && !FogStripper::world_is_host(layerMan.drawP.world)) return; + if(!FogStripper::world_is_host(layerMan.drawP.world) && !layerMan.own_peer_may(CollabRight::Hide, this)) + return; if(displayData && displayData->visible != newVisible) { displayData->visible = newVisible; layerMan.drawP.drawCache.clear_own_cached_surfaces(); @@ -304,12 +332,23 @@ void DrawingProgramLayerListItem::set_player_locked(DrawingProgramLayerManager& if(!displayData) return; bool newLocked = locked || displayData->fogLayer; - if(displayData->playerLocked != newLocked) { - displayData->playerLocked = newLocked; - layerMan.drawP.drawCache.clear_own_cached_surfaces(); - layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all(displayData, false); - layerMan.refresh_fog_client_projection(); + CollabPermissionSet nextDefaults = displayData->layerDefaults; + if(newLocked) + nextDefaults = CollabPermissionSet::content_deny(); + else if(displayData->playerLocked) { + // Unlock: clear content Deny back to Inherit for Draw/Move/Delete. + nextDefaults.draw = CollabTriState::Inherit; + nextDefaults.move = CollabTriState::Inherit; + nextDefaults.erase = CollabTriState::Inherit; } + if(displayData->playerLocked == newLocked && displayData->layerDefaults == nextDefaults) + return; + displayData->playerLocked = newLocked; + displayData->layerDefaults = nextDefaults; + layerMan.drawP.drawCache.clear_own_cached_surfaces(); + layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all(displayData, false); + layerMan.refresh_fog_client_projection(); + layerMan.ensure_editing_layer_is_editable(); } bool DrawingProgramLayerListItem::get_player_locked() const { @@ -335,6 +374,55 @@ bool DrawingProgramLayerListItem::is_player_edit_blocked() const { return is_fog_layer() || get_player_locked(); } +const CollabPermissionSet& DrawingProgramLayerListItem::get_layer_defaults() const { + static const CollabPermissionSet kInherit = CollabPermissionSet::all_inherit(); + return displayData ? displayData->layerDefaults : kInherit; +} + +void DrawingProgramLayerListItem::set_layer_defaults(DrawingProgramLayerManager& layerMan, const CollabPermissionSet& defaults) const { + if(!displayData || displayData->layerDefaults == defaults) + return; + displayData->layerDefaults = defaults; + displayData->sync_player_locked_from_defaults(); + layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all(displayData, false); + layerMan.ensure_editing_layer_is_editable(); +} + +const std::map& DrawingProgramLayerListItem::get_person_layer_overrides() const { + static const std::map kEmpty; + return displayData ? displayData->personOverrides : kEmpty; +} + +void DrawingProgramLayerListItem::set_person_layer_override(DrawingProgramLayerManager& layerMan, const std::string& displayName, const CollabPermissionSet& overrides) const { + if(!displayData || displayName.empty()) + return; + if(!overrides.any_concrete()) { + clear_person_layer_override(layerMan, displayName); + return; + } + auto it = displayData->personOverrides.find(displayName); + if(it != displayData->personOverrides.end() && it->second == overrides) + return; + displayData->personOverrides[displayName] = overrides; + layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all(displayData, false); + layerMan.ensure_editing_layer_is_editable(); +} + +void DrawingProgramLayerListItem::clear_person_layer_override(DrawingProgramLayerManager& layerMan, const std::string& displayName) const { + if(!displayData) + return; + if(displayData->personOverrides.erase(displayName) == 0) + return; + layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all(displayData, false); + layerMan.ensure_editing_layer_is_editable(); +} + +void DrawingProgramLayerListItem::apply_new_layer_permission_defaults(DrawingProgramLayerManager& layerMan) const { + if(!displayData || is_folder()) + return; + set_layer_defaults(layerMan, layerMan.drawP.collab_defaults().newLayerDefaults); +} + void DrawingProgramLayerListItem::set_metainfo(DrawingProgramLayerManager& layerMan, const DrawingProgramLayerListItemMetaInfo& metaInfo) { set_blend_mode(layerMan, metaInfo.blendMode); set_alpha(layerMan, metaInfo.alpha); @@ -403,7 +491,7 @@ void DrawingProgramLayerListItem::register_class(World& w) { .readUpdate = [](DisplayData& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr& c) { DisplayData incoming; a(incoming); - // Client→host: never allow stripping fog, unlocking, hiding, or fading fog. + // Client→host: never allow stripping fog, unlocking, hiding, fading fog, or escalating ACL. if(c) { if(o.fogLayer || incoming.fogLayer) { incoming.fogLayer = true; @@ -415,8 +503,11 @@ void DrawingProgramLayerListItem::register_class(World& w) { incoming.fogLayer = false; incoming.playerLocked = o.playerLocked; } + incoming.layerDefaults = o.layerDefaults; + incoming.personOverrides = o.personOverrides; } o = incoming; + o.sync_player_locked_from_defaults(); }, .postUpdateFunc = [&](DisplayData& o) { (void)o; diff --git a/src/DrawingProgram/Layers/DrawingProgramLayerListItem.hpp b/src/DrawingProgram/Layers/DrawingProgramLayerListItem.hpp index 9f13dab..2b99695 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayerListItem.hpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayerListItem.hpp @@ -20,10 +20,12 @@ #include #include #include +#include #include #include "DrawingProgramLayerFolder.hpp" #include "DrawingProgramLayer.hpp" #include "SerializedBlendMode.hpp" +#include "../../CollabPermissions.hpp" #include "../../CanvasComponents/CanvasComponentContainer.hpp" class DrawingProgramLayerFolder; @@ -89,6 +91,14 @@ class DrawingProgramLayerListItem { // Fog layers are always treated as player-locked for edit checks. bool is_player_edit_blocked() const; + const CollabPermissionSet& get_layer_defaults() const; + void set_layer_defaults(DrawingProgramLayerManager& layerMan, const CollabPermissionSet& defaults) const; + const std::map& get_person_layer_overrides() const; + void set_person_layer_override(DrawingProgramLayerManager& layerMan, const std::string& displayName, const CollabPermissionSet& overrides) const; + void clear_person_layer_override(DrawingProgramLayerManager& layerMan, const std::string& displayName) const; + /** Apply session new-layer defaults (call after create). */ + void apply_new_layer_permission_defaults(DrawingProgramLayerManager& layerMan) const; + void set_metainfo(DrawingProgramLayerManager& layerMan, const DrawingProgramLayerListItemMetaInfo& metaInfo); DrawingProgramLayerListItemMetaInfo get_metainfo() const; @@ -115,9 +125,13 @@ class DrawingProgramLayerListItem { SerializedBlendMode blendMode = SerializedBlendMode::BLEND_SRC_OVER; bool playerLocked = false; bool fogLayer = false; + CollabPermissionSet layerDefaults = CollabPermissionSet::all_inherit(); + std::map personOverrides; template void serialize(Archive& a) { - a(alpha, visible, blendMode, playerLocked, fogLayer); + a(alpha, visible, blendMode, playerLocked, fogLayer, layerDefaults, personOverrides); } + void migrate_player_locked_into_defaults(); + void sync_player_locked_from_defaults(); }; NetworkingObjects::NetObjOwnerPtr nameData; NetworkingObjects::NetObjOwnerPtr displayData; diff --git a/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp b/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp index d1a6947..2ae95ab 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp @@ -19,6 +19,7 @@ #include "DrawingProgramLayerManager.hpp" #include "FogStripper.hpp" #include "../../World.hpp" +#include "../../ClientData.hpp" #include "Helpers/NetworkingObjects/NetObjID.hpp" #include "Helpers/NetworkingObjects/NetObjOrderedList.hpp" #include "Helpers/NetworkingObjects/NetObjOwnerPtr.decl.hpp" @@ -291,20 +292,88 @@ void DrawingProgramLayerManager::disable_add_to_cache_and_commit_update_block(co addToCacheOnComponentInsert = true; } -bool DrawingProgramLayerManager::can_current_peer_edit_layer(const DrawingProgramLayerListItem* layer) const { +bool DrawingProgramLayerManager::peer_may(CollabRight right, const DrawingProgramLayerListItem* layer, const ClientData* peer) const { if(!layer) return false; + if(FogStripper::world_is_host(drawP.world) && (!peer || peer == drawP.world.ownClientData.get())) + return true; + // Content rights on fog layers are always denied for non-host peers. + if(layer->is_fog_layer() + && (right == CollabRight::Draw || right == CollabRight::Move || right == CollabRight::Delete)) + return false; + if(right == CollabRight::Hide && layer->is_fog_layer()) + return false; + + CollabPermissionSet personDefaults = CollabPermissionSet::all_inherit(); + const CollabPermissionSet* personLayer = nullptr; + std::string peerName; + if(peer) { + personDefaults = peer->get_person_defaults(); + peerName = peer->get_display_name(); + auto& overrides = layer->get_person_layer_overrides(); + auto it = overrides.find(peerName); + if(it != overrides.end()) + personLayer = &it->second; + } + return collab_resolve_allow( + right, + personLayer, + layer->get_layer_defaults(), + personDefaults, + drawP.collab_defaults().worldDefault); +} + +bool DrawingProgramLayerManager::own_peer_may(CollabRight right, const DrawingProgramLayerListItem* layer) const { if(FogStripper::world_is_host(drawP.world)) return true; - return !layer->is_player_edit_blocked(); + return peer_may(right, layer, drawP.world.ownClientData.get()); +} + +bool DrawingProgramLayerManager::peer_may_manage_layers(const ClientData* peer) const { + if(FogStripper::world_is_host(drawP.world) && (!peer || peer == drawP.world.ownClientData.get())) + return true; + // ManageLayers is not layer-scoped — resolve with empty layer defaults / no person×layer. + CollabPermissionSet personDefaults = peer ? peer->get_person_defaults() : CollabPermissionSet::all_inherit(); + return collab_resolve_allow( + CollabRight::ManageLayers, + nullptr, + CollabPermissionSet::all_inherit(), + personDefaults, + drawP.collab_defaults().worldDefault); +} + +bool DrawingProgramLayerManager::current_net_client_may(CollabRight right, const DrawingProgramLayerListItem* layer) const { + if(!drawP.world.currentNetUpdateClient) + return true; // local/host-originated + auto peer = drawP.world.netObjMan.get_obj_temporary_ref_from_id( + NetworkingObjects::NetObjID(drawP.world.currentNetUpdateClient->customID)); + if(!peer) + return false; + if(right == CollabRight::ManageLayers) + return peer_may_manage_layers(peer.get()); + return peer_may(right, layer, peer.get()); +} + +bool DrawingProgramLayerManager::can_current_peer_edit_layer(const DrawingProgramLayerListItem* layer) const { + return own_peer_may(CollabRight::Draw, layer); } bool DrawingProgramLayerManager::client_may_modify_component(const CanvasComponentContainer* comp) const { + return client_may_modify_component(comp, CollabRight::Draw); +} + +bool DrawingProgramLayerManager::client_may_modify_component(const CanvasComponentContainer* comp, CollabRight right) const { // Host-side authority check for CLIENT messages — must NOT use can_current_peer_edit_layer // (that always returns true on the host). if(!comp || !comp->parentLayer) return false; - return !comp->parentLayer->is_player_edit_blocked(); + if(!drawP.world.currentNetUpdateClient) { + // Transform callbacks set currentNetUpdateClient; fallback: legacy lock/fog. + if(comp->parentLayer->is_player_edit_blocked()) + return false; + return true; + } + return current_net_client_may(right, comp->parentLayer); } void DrawingProgramLayerManager::ensure_editing_layer_is_editable() { diff --git a/src/DrawingProgram/Layers/DrawingProgramLayerManager.hpp b/src/DrawingProgram/Layers/DrawingProgramLayerManager.hpp index 6c22977..a0bcda3 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayerManager.hpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayerManager.hpp @@ -19,9 +19,12 @@ #pragma once #include "DrawingProgramLayerListItem.hpp" #include "DrawingProgramLayerManagerGUI.hpp" +#include "../../CollabPermissions.hpp" #include #include +class ClientData; + class DrawingProgramLayerManager { private: DrawingProgram& drawP; @@ -89,6 +92,11 @@ class DrawingProgramLayerManager { bool can_current_peer_edit_layer(const DrawingProgramLayerListItem* layer) const; bool client_may_modify_component(const CanvasComponentContainer* comp) const; + bool client_may_modify_component(const CanvasComponentContainer* comp, CollabRight right) const; + bool own_peer_may(CollabRight right, const DrawingProgramLayerListItem* layer) const; + bool peer_may(CollabRight right, const DrawingProgramLayerListItem* layer, const ClientData* peer) const; + bool peer_may_manage_layers(const ClientData* peer) const; + bool current_net_client_may(CollabRight right, const DrawingProgramLayerListItem* layer) const; void ensure_editing_layer_is_editable(); /** Host: recompute fog coverage and erase/reveal (+ resources) on clients. */ void refresh_fog_client_projection(); diff --git a/src/DrawingProgram/Layers/DrawingProgramLayerManagerGUI.cpp b/src/DrawingProgram/Layers/DrawingProgramLayerManagerGUI.cpp index 8d44030..32a6020 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayerManagerGUI.cpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayerManagerGUI.cpp @@ -26,6 +26,8 @@ #include "Helpers/ConvertVec.hpp" #include "Helpers/NetworkingObjects/NetObjOrderedList.hpp" #include "SerializedBlendMode.hpp" +#include "../../CollabPermissions.hpp" +#include "../../ClientData.hpp" #include "../../GUIStuff/ElementHelpers/TextLabelHelpers.hpp" #include "../../GUIStuff/ElementHelpers/ButtonHelpers.hpp" @@ -150,20 +152,22 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .size = TreeListing::ENTRY_HEIGHT, .onClick = [&, layer, fogVisibilityLocked] { - if(!fogVisibilityLocked) + if(!fogVisibilityLocked && layerMan.own_peer_may(CollabRight::Hide, layer.get())) layer->set_visible(layerMan, !layer->get_visible()); } }); }); - gui.set_z_index_keep_clipping_region(gui.get_z_index() + 1, [&] { - svg_icon_button(gui, "delete button", "data/icons/trash.svg", { - .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, - .size = TreeListing::ENTRY_HEIGHT, - .onClick = [&, objIndex] { - remove_layer(objIndex); - } + if(layerMan.peer_may_manage_layers(world.ownClientData.get())) { + gui.set_z_index_keep_clipping_region(gui.get_z_index() + 1, [&] { + svg_icon_button(gui, "delete button", "data/icons/trash.svg", { + .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, + .size = TreeListing::ENTRY_HEIGHT, + .onClick = [&, objIndex] { + remove_layer(objIndex); + } + }); }); - }); + } }, .onDoubleClick = [&](const TreeListingObjIndexList& objIndex) { auto layer = get_layer_from_obj_index(objIndex); @@ -196,6 +200,8 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() { } }, .moveObj = [&](const std::vector& objectIndicesTreeListing, const TreeListingObjIndexList& newObjIndexTreeListing) { + if(!layerMan.peer_may_manage_layers(world.ownClientData.get())) + return; NetObjID listObj; size_t index; struct ParentObjectIDPair { @@ -396,32 +402,34 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() { }); } - left_to_right_line_layout(gui, [&]() { - input_text(gui, "input new name", &nameForNew, { - .onEnter = [&] { - auto newLayerObjInfo = create_layer(new DrawingProgramLayerListItem(world.netObjMan, nameForNew, false)); - layerMan.editingLayer = newLayerObjInfo->obj; - layerMan.drawP.world.main.g.gui.set_to_layout(); - } - }); + if(layerMan.peer_may_manage_layers(world.ownClientData.get())) { + left_to_right_line_layout(gui, [&]() { + input_text(gui, "input new name", &nameForNew, { + .onEnter = [&] { + auto newLayerObjInfo = create_layer(new DrawingProgramLayerListItem(world.netObjMan, nameForNew, false)); + layerMan.editingLayer = newLayerObjInfo->obj; + layerMan.drawP.world.main.g.gui.set_to_layout(); + } + }); - svg_icon_button(gui, "create new layer", "data/icons/plusbold.svg", { - .size = SMALL_BUTTON_SIZE, - .onClick = [&] { - auto newLayerObjInfo = create_layer(new DrawingProgramLayerListItem(world.netObjMan, nameForNew, false)); - layerMan.editingLayer = newLayerObjInfo->obj; - layerMan.drawP.world.main.g.gui.set_to_layout(); - } - }); + svg_icon_button(gui, "create new layer", "data/icons/plusbold.svg", { + .size = SMALL_BUTTON_SIZE, + .onClick = [&] { + auto newLayerObjInfo = create_layer(new DrawingProgramLayerListItem(world.netObjMan, nameForNew, false)); + layerMan.editingLayer = newLayerObjInfo->obj; + layerMan.drawP.world.main.g.gui.set_to_layout(); + } + }); - svg_icon_button(gui, "create new folder", "data/icons/folderbold.svg", { - .size = SMALL_BUTTON_SIZE, - .onClick = [&] { - auto newFolderObjInfo = create_layer(new DrawingProgramLayerListItem(world.netObjMan, nameForNew, true)); - newFolderObjInfo->obj->get_folder().isFolderOpen = true; - } + svg_icon_button(gui, "create new folder", "data/icons/folderbold.svg", { + .size = SMALL_BUTTON_SIZE, + .onClick = [&] { + auto newFolderObjInfo = create_layer(new DrawingProgramLayerListItem(world.netObjMan, nameForNew, true)); + newFolderObjInfo->obj->get_folder().isFolderOpen = true; + } + }); }); - }); + } auto editingLayerLock = editingLayer.lock(); if(editingLayerLock) { @@ -479,6 +487,67 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() { if(layer) layer->set_player_locked(layerMan, !layer->get_player_locked()); }); + text_label(gui, "Layer defaults (Inherit → Person → World)"); + { + const CollabPermissionSet& layerSet = editingLayerLock->get_layer_defaults(); + left_to_right_line_layout(gui, [&]() { + for(uint8_t i = 0; i < static_cast(CollabRight::COUNT); i++) { + CollabRight right = static_cast(i); + gui.new_id(200 + i, [&] { + std::string label = std::string(collab_right_label(right)) + ":" + + collab_tristate_label(layerSet.get(right)); + text_button(gui, "layer perm", label.c_str(), { + .onClick = [&, right] { + auto layer = editingLayer.lock(); + if(!layer) + return; + CollabPermissionSet next = layer->get_layer_defaults(); + next.set(right, collab_cycle_tristate(next.get(right))); + layer->set_layer_defaults(layerMan, next); + } + }); + }); + } + }); + } + if(world.clients) { + text_label(gui, "Per-player overrides on this layer"); + size_t pnum = 0; + for(auto& client : world.clients->get_data()) { + if(client == world.ownClientData) + continue; + gui.new_id(300 + pnum++, [&] { + const std::string pname = client->get_display_name(); + text_label(gui, pname); + CollabPermissionSet overrideSet = CollabPermissionSet::all_inherit(); + auto it = editingLayerLock->get_person_layer_overrides().find(pname); + if(it != editingLayerLock->get_person_layer_overrides().end()) + overrideSet = it->second; + left_to_right_line_layout(gui, [&]() { + for(uint8_t i = 0; i < static_cast(CollabRight::COUNT); i++) { + CollabRight right = static_cast(i); + gui.new_id(i, [&] { + std::string label = std::string(collab_right_label(right)) + ":" + + collab_tristate_label(overrideSet.get(right)); + text_button(gui, "player layer perm", label.c_str(), { + .onClick = [&, right, pname] { + auto layer = editingLayer.lock(); + if(!layer) + return; + CollabPermissionSet next = CollabPermissionSet::all_inherit(); + auto found = layer->get_person_layer_overrides().find(pname); + if(found != layer->get_person_layer_overrides().end()) + next = found->second; + next.set(right, collab_cycle_tristate(next.get(right))); + layer->set_person_layer_override(layerMan, pname, next); + } + }); + }); + } + }); + }); + } + } } else text_label(gui, "Fog layers are always locked for players."); @@ -563,6 +632,7 @@ NetworkingObjects::NetObjOrderedListIterator Drawin auto insertedLayerPair = create_in_proper_position(newItem); auto& it = insertedLayerPair.second; + it->obj->apply_new_layer_permission_defaults(layerMan); world.undo.push(std::make_unique(std::make_unique(it->obj->get_undo_data(world.undo)), it->pos, world.undo.get_undoid_from_netid(insertedLayerPair.first), world.undo.get_undoid_from_netid(it->obj.get_net_id()))); world.main.g.gui.set_to_layout(); refresh_gui_data(); diff --git a/src/Toolbar.cpp b/src/Toolbar.cpp index 9cc1f4d..145fcdc 100644 --- a/src/Toolbar.cpp +++ b/src/Toolbar.cpp @@ -36,6 +36,8 @@ #include "RichText/TextStyleModifier.hpp" #include "VersionConstants.hpp" #include "World.hpp" +#include "ClientData.hpp" +#include "CollabPermissions.hpp" #include #include #include @@ -1205,33 +1207,84 @@ void Toolbar::color_palette(const char* id, Vector4f* color, const std::function } void Toolbar::player_list() { + using namespace GUIStuff; + using namespace ElementHelpers; auto& gui = main.g.gui; + auto& world = *main.world; + auto& layerMan = world.drawProg.layerMan; - center_obstructing_window_gui("player client list", CLAY_SIZING_FIT(500), CLAY_SIZING_FIT(0), [&] { + auto draw_perm_buttons = [&](const char* rowId, const CollabPermissionSet& shown, + const std::function& cycleRight) { + gui.new_id(rowId, [&] { + left_to_right_line_layout(gui, [&]() { + for(uint8_t i = 0; i < static_cast(CollabRight::COUNT); i++) { + CollabRight right = static_cast(i); + gui.new_id(i, [&] { + std::string label = std::string(collab_right_label(right)) + ":" + + collab_tristate_label(shown.get(right)); + text_button(gui, "perm", label.c_str(), { + .onClick = [cycleRight, right] { + cycleRight(right); + } + }); + }); + } + }); + }); + }; + + center_obstructing_window_gui("player client list", CLAY_SIZING_FIT(640), CLAY_SIZING_FIT(0), [&] { gui.new_id("client list", [&] { text_label_centered(gui, "Player List"); - if(!main.world->clientStillConnecting) { - if(main.world->netServer) { + if(!world.clientStillConnecting) { + if(world.netServer) { text_button(gui, "force all jump to host", "Bring Everyone Here", { .wide = true, .onClick = [&] { - main.world->drawProg.force_clients_to_own_view(); + world.drawProg.force_clients_to_own_view(); } }); + text_button(gui, "toggle session defaults", collabSessionDefaultsOpen ? "Hide Session Defaults" : "Session Defaults…", { + .wide = true, + .onClick = [&] { + collabSessionDefaultsOpen = !collabSessionDefaultsOpen; + } + }); + if(collabSessionDefaultsOpen) { + const CollabSessionDefaults& cur = world.drawProg.collab_defaults(); + text_label(gui, "World default (fallback when everything inherits)"); + draw_perm_buttons("world default row", cur.worldDefault, [&](CollabRight right) { + auto d = world.drawProg.collab_defaults(); + d.worldDefault.set(right, collab_cycle_tristate(d.worldDefault.get(right))); + world.drawProg.set_collab_defaults(d); + }); + text_label(gui, "New player defaults (copied on join)"); + draw_perm_buttons("new player default row", cur.newPlayerDefaults, [&](CollabRight right) { + auto d = world.drawProg.collab_defaults(); + d.newPlayerDefaults.set(right, collab_cycle_tristate(d.newPlayerDefaults.get(right))); + world.drawProg.set_collab_defaults(d); + }); + text_label(gui, "New layer defaults (copied on create)"); + draw_perm_buttons("new layer default row", cur.newLayerDefaults, [&](CollabRight right) { + auto d = world.drawProg.collab_defaults(); + d.newLayerDefaults.set(right, collab_cycle_tristate(d.newLayerDefaults.get(right))); + world.drawProg.set_collab_defaults(d); + }); + } } left_to_right_line_layout(gui, [&]() { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)} }, - .backgroundColor = convert_vec4(SkColor4f{main.world->ownClientData->get_cursor_color().x(), main.world->ownClientData->get_cursor_color().y(), main.world->ownClientData->get_cursor_color().z(), 1.0f}), + .backgroundColor = convert_vec4(SkColor4f{world.ownClientData->get_cursor_color().x(), world.ownClientData->get_cursor_color().y(), world.ownClientData->get_cursor_color().z(), 1.0f}), .cornerRadius = CLAY_CORNER_RADIUS(3) }) {} - text_label(gui, main.world->ownClientData->get_display_name()); + text_label(gui, world.ownClientData->get_display_name() + " (you / host)"); }); size_t num = 0; - for(auto& client : main.world->clients->get_data()) { - if(client != main.world->ownClientData) { + for(auto& client : world.clients->get_data()) { + if(client != world.ownClientData) { gui.new_id(num++, [&] { left_to_right_line_layout(gui, [&]() { CLAY_AUTO_ID({ @@ -1244,15 +1297,40 @@ void Toolbar::player_list() { text_label(gui, client->get_display_name()); CLAY_AUTO_ID({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}}) {} text_button(gui, "teleport button", "Jump To", { .onClick = [&] { - main.world->drawData.cam.smooth_move_to(*main.world, client->get_cam_coords(), client->get_window_size()); + world.drawData.cam.smooth_move_to(world, client->get_cam_coords(), client->get_window_size()); }}); + if(world.netServer) { + text_button(gui, "perms button", "Rights", { + .onClick = [&, name = client->get_display_name()] { + if(collabPermsExpandedPlayer == name) + collabPermsExpandedPlayer.clear(); + else + collabPermsExpandedPlayer = name; + } + }); + } }); + if(world.netServer && collabPermsExpandedPlayer == client->get_display_name()) { + text_label(gui, "Person defaults (Inherit → World)"); + NetworkingObjects::NetObjID clientId = client.get_net_id(); + draw_perm_buttons("person defaults", client->get_person_defaults(), [&, clientId](CollabRight right) { + auto peer = world.netObjMan.get_obj_temporary_ref_from_id(clientId); + if(!peer) + return; + CollabPermissionSet next = peer->get_person_defaults(); + next.set(right, collab_cycle_tristate(next.get(right))); + ClientData::set_person_defaults(peer, next); + layerMan.ensure_editing_layer_is_editable(); + }); + } }); } } } text_button(gui, "close list", "Done", { .wide = true, .onClick = [&] { playerMenuOpen = false; + collabSessionDefaultsOpen = false; + collabPermsExpandedPlayer.clear(); }}); }); }); diff --git a/src/Toolbar.hpp b/src/Toolbar.hpp index 05d4438..a243e8a 100644 --- a/src/Toolbar.hpp +++ b/src/Toolbar.hpp @@ -153,6 +153,8 @@ class Toolbar { bool menuPopUpOpen = false; bool optionsMenuOpen = false; bool playerMenuOpen = false; + bool collabSessionDefaultsOpen = false; + std::string collabPermsExpandedPlayer; float finalCalculatedGuiScale = 1.0f; diff --git a/src/VersionConstants.cpp b/src/VersionConstants.cpp index 2138525..1540978 100644 --- a/src/VersionConstants.cpp +++ b/src/VersionConstants.cpp @@ -32,6 +32,7 @@ VersionNumber header_to_version_number(const std::string& header) { m["INFPNT000006"] = VersionNumber(0, 6, 0); m["INFPNT000007"] = VersionNumber(0, 7, 0); m["INFPNT000008"] = VersionNumber(0, 8, 0); + m["INFPNT000009"] = VersionNumber(0, 9, 0); } auto it = m.find(header); if(it == m.end()) diff --git a/src/VersionConstants.hpp b/src/VersionConstants.hpp index d01c73f..4dfb13d 100644 --- a/src/VersionConstants.hpp +++ b/src/VersionConstants.hpp @@ -28,7 +28,7 @@ namespace VersionConstants { VersionNumber header_to_version_number(const std::string& header); constexpr int SAVEFILE_HEADER_LEN = 12; // DO NOT CHANGE THIS HEADER LENGTH - const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000008"; // Change whenever the save file is incompatible with the previous version - const std::string CURRENT_VERSION_STRING = "0.8.0-fork"; - constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 8, 0); + const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000009"; // Change whenever the save file is incompatible with the previous version + const std::string CURRENT_VERSION_STRING = "0.9.0-fork"; + constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 9, 0); } diff --git a/src/World.cpp b/src/World.cpp index 3cecd46..0a23226 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -70,6 +70,7 @@ World::World(MainProgram& initMain, const CustomEvents::OpenInfiniPaintFileEvent saveThumbnail = worldInfo.saveThumbnail; init_net_obj_type_list(); + drawProg.ensure_collab_defaults(); netObjMan.set_netobj_destroy_callback([&undo = undo](const NetworkingObjects::NetObjID& idToDestroy) { undo.remove_by_netid(idToDestroy); }); @@ -135,6 +136,7 @@ void World::init_net_obj_type_list() { NetworkingObjects::register_ordered_list_class(netObjMan); DrawingProgramLayerListItem::register_class(*this); NetworkingObjects::register_ordered_list_class(netObjMan); + DrawingProgram::register_collab_defaults_class(*this); canvasTheme.register_class(); ClientData::register_class(*this); NetworkingObjects::register_unordered_set_class(netObjMan); @@ -433,6 +435,7 @@ void World::start_hosting(const std::string& initNetSource, const std::string& s newClientData.camCoords = ownClientData->get_cam_coords(); newClientData.windowSize = ownClientData->get_window_size(); newClientData.gridSize = ownClientData->get_grid_size(); + newClientData.personDefaults = drawProg.collab_defaults().newPlayerDefaults; NetworkingObjects::NetObjTemporaryPtr clientDataObjPtr = clients->emplace_direct(clients, newClientData); client->customID = clientDataObjPtr.get_net_id().data;