Prevent players from hiding Fog of War layers
Some checks are pending
Windows Release / windows-x86_64 (push) Waiting to run

Clients could toggle fog visibility (and fade alpha); DisplayData synced
that to everyone and defeated FoW. Block client set_visible/set_alpha,
clamp host-side DisplayData updates, and ignore eye toggles for fog.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
copilot 2026-11-07 15:42:58 +01:00
parent fea392b65d
commit 3140f9bb55
4 changed files with 31 additions and 3 deletions

View file

@ -33,4 +33,4 @@ Not automatic on import. Choose **Paste Image + Freistellen** from the canvas ri
## Fog of War / layer lock (host) ## 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). 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.

0
linuxinstall/install-local.sh Normal file → Executable file
View file

View file

@ -20,6 +20,7 @@
#include "DrawingProgramLayerFolder.hpp" #include "DrawingProgramLayerFolder.hpp"
#include <cereal/types/string.hpp> #include <cereal/types/string.hpp>
#include "DrawingProgramLayer.hpp" #include "DrawingProgramLayer.hpp"
#include "FogStripper.hpp"
#include "../../World.hpp" #include "../../World.hpp"
#include "../../MainProgram.hpp" #include "../../MainProgram.hpp"
#include "SerializedBlendMode.hpp" #include "SerializedBlendMode.hpp"
@ -258,6 +259,9 @@ const std::string& DrawingProgramLayerListItem::get_name() const {
} }
void DrawingProgramLayerListItem::set_alpha(DrawingProgramLayerManager& layerMan, float newAlpha) const { void DrawingProgramLayerListItem::set_alpha(DrawingProgramLayerManager& layerMan, float newAlpha) const {
// Players must not fade fog out — same authority as visibility.
if(is_fog_layer() && !FogStripper::world_is_host(layerMan.drawP.world))
return;
if(displayData && displayData->alpha != newAlpha) { if(displayData && displayData->alpha != newAlpha) {
displayData->alpha = newAlpha; displayData->alpha = newAlpha;
layerMan.drawP.drawCache.clear_own_cached_surfaces(); layerMan.drawP.drawCache.clear_own_cached_surfaces();
@ -270,6 +274,9 @@ float DrawingProgramLayerListItem::get_alpha() const {
} }
void DrawingProgramLayerListItem::set_visible(DrawingProgramLayerManager& layerMan, bool newVisible) const { void DrawingProgramLayerListItem::set_visible(DrawingProgramLayerManager& layerMan, bool newVisible) const {
// 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(displayData && displayData->visible != newVisible) { if(displayData && displayData->visible != newVisible) {
displayData->visible = newVisible; displayData->visible = newVisible;
layerMan.drawP.drawCache.clear_own_cached_surfaces(); layerMan.drawP.drawCache.clear_own_cached_surfaces();
@ -393,6 +400,24 @@ void DrawingProgramLayerListItem::register_class(World& w) {
} }
}); });
w.delayedUpdateObjectManager.register_class<DisplayData>(w.netObjMan, NetworkingObjects::DelayUpdateSerializedClassManager::CustomConstructors<DisplayData>{ w.delayedUpdateObjectManager.register_class<DisplayData>(w.netObjMan, NetworkingObjects::DelayUpdateSerializedClassManager::CustomConstructors<DisplayData>{
.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.
if(c) {
if(o.fogLayer || incoming.fogLayer) {
incoming.fogLayer = true;
incoming.playerLocked = true;
incoming.visible = true;
incoming.alpha = o.alpha;
}
else {
incoming.fogLayer = false;
incoming.playerLocked = o.playerLocked;
}
}
o = incoming;
},
.postUpdateFunc = [&](DisplayData& o) { .postUpdateFunc = [&](DisplayData& o) {
(void)o; (void)o;
w.drawProg.drawCache.clear_own_cached_surfaces(); w.drawProg.drawCache.clear_own_cached_surfaces();

View file

@ -144,11 +144,14 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() {
}); });
} }
gui.set_z_index_keep_clipping_region(gui.get_z_index() + 1, [&] { gui.set_z_index_keep_clipping_region(gui.get_z_index() + 1, [&] {
// Fog stays visible for players — eye is informational only on clients.
const bool fogVisibilityLocked = layer->is_fog_layer() && !FogStripper::world_is_host(world);
svg_icon_button(gui, "visible button", layer->get_visible() ? "data/icons/eyeopen.svg" : "data/icons/eyeclose.svg", { svg_icon_button(gui, "visible button", layer->get_visible() ? "data/icons/eyeopen.svg" : "data/icons/eyeclose.svg", {
.drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .drawType = SelectableButton::DrawType::TRANSPARENT_ALL,
.size = TreeListing::ENTRY_HEIGHT, .size = TreeListing::ENTRY_HEIGHT,
.onClick = [&, layer] { .onClick = [&, layer, fogVisibilityLocked] {
layer->set_visible(layerMan, !layer->get_visible()); if(!fogVisibilityLocked)
layer->set_visible(layerMan, !layer->get_visible());
} }
}); });
}); });