Add host-configurable collab permissions with layered overrides.
All checks were successful
Windows Release / windows-x86_64 (push) Successful in 1h12m54s

Session/person/layer defaults plus person×layer ACL (Allow/Deny/Inherit) for draw, move, delete, hide, and layer management; bump save format to 0.9.0-fork.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
copilot 2026-11-07 18:12:42 +01:00
parent 2009a3f892
commit 7f34283537
20 changed files with 715 additions and 70 deletions

View file

@ -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"

18
FORK.md
View file

@ -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.

View file

@ -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<NetServer::ClientData>& 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<NetServer::ClientData>& 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>(CanvasComponent::allocate_comp(o.comp->get_type()));
a(*junk);
return;

View file

@ -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<ClientData>,
@ -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<ClientData>& 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<Clien
});
}
void ClientData::set_person_defaults(const NetworkingObjects::NetObjTemporaryPtr<ClientData>& o, const CollabPermissionSet& defaults) {
if(o->personDefaults == defaults)
return;
o->personDefaults = defaults;
o.send_update_to_all(RELIABLE_COMMAND_CHANNEL, [](const NetObjTemporaryPtr<ClientData>& 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();

View file

@ -19,6 +19,7 @@
#pragma once
#include <Helpers/Serializers.hpp>
#include "CoordSpaceHelper.hpp"
#include "CollabPermissions.hpp"
#include <Helpers/NetworkingObjects/NetObjTemporaryPtr.hpp>
#include <Helpers/NetworkingObjects/NetObjManager.hpp>
@ -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<ClientData>& o, const CoordSpaceHelper& newCoords);
static void send_chat_message(const NetworkingObjects::NetObjTemporaryPtr<ClientData>& o, World& world, const std::string& chatMessage);
static void scale_up_step(const NetworkingObjects::NetObjTemporaryPtr<ClientData>& o, World& world);
/** Host-only: push person-default ACL to all peers. */
static void set_person_defaults(const NetworkingObjects::NetObjTemporaryPtr<ClientData>& 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 <typename Archive> 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;
};

114
src/CollabPermissions.cpp Normal file
View file

@ -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;
}

77
src/CollabPermissions.hpp Normal file
View file

@ -0,0 +1,77 @@
/*
* InfiniPaint (fork)
* Collab ACL: World → Person → Layer → Person×Layer (Allow/Deny/Inherit)
*/
#pragma once
#include <cstdint>
#include <map>
#include <string>
#include <cereal/types/map.hpp>
#include <cereal/types/string.hpp>
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 <typename Archive>
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 <typename Archive>
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);

View file

@ -48,6 +48,8 @@
#include <chrono>
#include "../CanvasComponents/ImageCanvasComponent.hpp"
#include "Layers/DrawingProgramLayer.hpp"
#include "../CollabPermissions.hpp"
#include <Helpers/NetworkingObjects/DelayUpdateSerializedClassManager.hpp>
#include "../ImagePreprocess.hpp"
#include <Helpers/Random.hpp>
#include <cereal/types/utility.hpp>
@ -308,7 +310,44 @@ std::optional<InputManager::TextBoxStartInfo> DrawingProgram::get_text_box_start
return drawTool->get_text_box_start_info();
}
void DrawingProgram::register_collab_defaults_class(World& world) {
world.delayedUpdateObjectManager.register_class<CollabSessionDefaults>(world.netObjMan, NetworkingObjects::DelayUpdateSerializedClassManager::CustomConstructors<CollabSessionDefaults>{
.readUpdate = [](CollabSessionDefaults& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr<NetServer::ClientData>& 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<CollabSessionDefaults>();
}
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<CollabSessionDefaults>(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<CollabSessionDefaults>(a, nullptr);
layerMan.read_components_client(a);
drawCache.build({});
}
@ -335,6 +377,7 @@ void DrawingProgram::init_server_callbacks() {
std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>> transforms;
message(transforms);
world.currentNetUpdateClient = client;
auto clientData = world.netObjMan.get_obj_temporary_ref_from_id<ClientData>(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<CanvasComponentContainer>(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<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> groupIds;
message(groupIds);
(void)client;
world.currentNetUpdateClient = client;
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> allowed;
allowed.reserve(groupIds.size());
for(auto& [netID, groupId] : groupIds) {
auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer>(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::vector<CanvasComponentContai
for(auto& obj : objsToSendTransformsFor) {
if(!obj || !obj->obj)
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<DrawingProgram*>(this)->ensure_collab_defaults();
a(*collabDefaults);
layerMan.save_file(a);
}

View file

@ -37,6 +37,7 @@
#include <Helpers/NetworkingObjects/NetObjOrderedList.hpp>
#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<CollabSessionDefaults> 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);

View file

@ -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;
}

View file

@ -19,11 +19,13 @@
#include "DrawingProgramLayerListItem.hpp"
#include "DrawingProgramLayerFolder.hpp"
#include <cereal/types/string.hpp>
#include <cereal/types/map.hpp>
#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<NameData>();
a(*nameData);
displayData = layerMan.drawP.world.netObjMan.make_obj<DisplayData>();
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>(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>(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>(displayData, false);
layerMan.ensure_editing_layer_is_editable();
}
const std::map<std::string, CollabPermissionSet>& DrawingProgramLayerListItem::get_person_layer_overrides() const {
static const std::map<std::string, CollabPermissionSet> 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>(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>(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<NetServer::ClientData>& 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;

View file

@ -20,10 +20,12 @@
#include <Helpers/NetworkingObjects/NetObjManager.hpp>
#include <Helpers/NetworkingObjects/NetObjOrderedList.hpp>
#include <Helpers/NetworkingObjects/DelayUpdateSerializedClassManager.hpp>
#include <map>
#include <unordered_set>
#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<std::string, CollabPermissionSet>& 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<std::string, CollabPermissionSet> personOverrides;
template <typename Archive> 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> nameData;
NetworkingObjects::NetObjOwnerPtr<DisplayData> displayData;

View file

@ -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<ClientData>(
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() {

View file

@ -19,9 +19,12 @@
#pragma once
#include "DrawingProgramLayerListItem.hpp"
#include "DrawingProgramLayerManagerGUI.hpp"
#include "../../CollabPermissions.hpp"
#include <unordered_set>
#include <vector>
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();

View file

@ -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<TreeListingObjIndexList>& 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<uint8_t>(CollabRight::COUNT); i++) {
CollabRight right = static_cast<CollabRight>(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<uint8_t>(CollabRight::COUNT); i++) {
CollabRight right = static_cast<CollabRight>(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<DrawingProgramLayerListItem> 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<AddLayerWorldUndoAction>(std::make_unique<DrawingProgramLayerListItemUndoData>(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();

View file

@ -36,6 +36,8 @@
#include "RichText/TextStyleModifier.hpp"
#include "VersionConstants.hpp"
#include "World.hpp"
#include "ClientData.hpp"
#include "CollabPermissions.hpp"
#include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_filesystem.h>
#include <SDL3/SDL_render.h>
@ -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<void(CollabRight)>& cycleRight) {
gui.new_id(rowId, [&] {
left_to_right_line_layout(gui, [&]() {
for(uint8_t i = 0; i < static_cast<uint8_t>(CollabRight::COUNT); i++) {
CollabRight right = static_cast<CollabRight>(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<Clay_Color>(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<Clay_Color>(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<ClientData>(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();
}});
});
});

View file

@ -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;

View file

@ -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())

View file

@ -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);
}

View file

@ -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<CanvasComponentContainer>(netObjMan);
DrawingProgramLayerListItem::register_class(*this);
NetworkingObjects::register_ordered_list_class<DrawingProgramLayerListItem>(netObjMan);
DrawingProgram::register_collab_defaults_class(*this);
canvasTheme.register_class();
ClientData::register_class(*this);
NetworkingObjects::register_unordered_set_class<ClientData>(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<ClientData> clientDataObjPtr = clients->emplace_direct(clients, newClientData);
client->customID = clientDataObjPtr.get_net_id().data;