Files
OpenFrontIO/src/server/Privilege.ts
T
evanpelle a26585a47b Add support for colored patterns (#2062)
## Description:

Add support for colored territory patterns/skins

* Refactored & updated territory pattern rendering to render colored
skins
* rename public from pattern to skin (keep pattern name internally, too
difficult to rename)
* Moved all territory color logic to PlayerView
* Updated WinModal to show colored skins
* Refactored decode logic into a separate function: decodePatternData
* Refactored/updated how cosmetics are sent to server. Players now send
a PlayerCosmeticRefsSchema in the ClientJoinMessage.
PlayerCosmeticRefsSchema just contains names of the cosmetics, and the
server replaces the names/references with actual cosmetic data
* Refactored PastelThemeDark: have it extend Pastel theme so duplicate
logic can be removed.
* 

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
2025-09-18 20:00:15 -07:00

157 lines
4.3 KiB
TypeScript

import { Cosmetics } from "../core/CosmeticSchemas";
import { decodePatternData } from "../core/PatternDecoder";
import { PlayerPattern } from "../core/Schemas";
type PatternResult =
| { type: "allowed"; pattern: PlayerPattern }
| { type: "unknown" }
| { type: "forbidden"; reason: string };
export interface PrivilegeChecker {
isPatternAllowed(
flares: readonly string[],
name: string,
colorPaletteName: string | null,
): PatternResult;
isCustomFlagAllowed(
flag: string,
flares: readonly string[] | undefined,
): true | "restricted" | "invalid";
}
export class PrivilegeCheckerImpl implements PrivilegeChecker {
constructor(
private cosmetics: Cosmetics,
private b64urlDecode: (base64: string) => Uint8Array,
) {}
isPatternAllowed(
flares: readonly string[],
name: string,
colorPaletteName: string | null,
): PatternResult {
// Look for the pattern in the cosmetics.json config
const found = this.cosmetics.patterns[name];
if (!found) return { type: "forbidden", reason: "pattern not found" };
try {
decodePatternData(found.pattern, this.b64urlDecode);
} catch (e) {
return { type: "forbidden", reason: "invalid pattern" };
}
const colorPalette = this.cosmetics.colorPalettes?.[colorPaletteName ?? ""];
if (flares.includes("pattern:*")) {
return {
type: "allowed",
pattern: { name: found.name, patternData: found.pattern, colorPalette },
};
}
const flareName =
`pattern:${found.name}` +
(colorPaletteName ? `:${colorPaletteName}` : "");
if (flares.includes(flareName)) {
// Player has a flare for this pattern
return {
type: "allowed",
pattern: { name: found.name, patternData: found.pattern, colorPalette },
};
} else {
return { type: "forbidden", reason: "no flares for pattern" };
}
}
isCustomFlagAllowed(
flag: string,
flares: readonly string[] | undefined,
): true | "restricted" | "invalid" {
if (!flag.startsWith("!")) return "invalid";
const code = flag.slice(1);
if (!code) return "invalid";
const segments = code.split("_");
if (segments.length === 0) return "invalid";
const MAX_LAYERS = 6; // Maximum number of layers allowed
if (segments.length > MAX_LAYERS) return "invalid";
const superFlare = flares?.includes("flag:*") ?? false;
for (const segment of segments) {
const [layerKey, colorKey] = segment.split("-");
if (!layerKey || !colorKey) return "invalid";
const layer = this.cosmetics.flag?.layers[layerKey];
const color = this.cosmetics.flag?.color[colorKey];
if (!layer || !color) return "invalid";
// Super-flare bypasses all restrictions
if (superFlare) {
continue;
}
// Check layer restrictions
const layerSpec = layer;
let layerAllowed = false;
if (!layerSpec.flares) {
layerAllowed = true;
} else {
// By flare
if (
layerSpec.flares &&
flares?.some((f) => layerSpec.flares?.includes(f))
) {
layerAllowed = true;
}
// By named flag:layer:{name}
if (flares?.includes(`flag:layer:${layerSpec.name}`)) {
layerAllowed = true;
}
}
// Check color restrictions
const colorSpec = color;
let colorAllowed = false;
if (!colorSpec.flares) {
colorAllowed = true;
} else {
// By flare
if (
colorSpec.flares &&
flares?.some((f) => colorSpec.flares?.includes(f))
) {
colorAllowed = true;
}
// By named flag:color:{name}
if (flares?.includes(`flag:color:${colorSpec.name}`)) {
colorAllowed = true;
}
}
// If either part is restricted, block
if (!(layerAllowed && colorAllowed)) {
return "restricted";
}
}
return true;
}
}
export class FailOpenPrivilegeChecker implements PrivilegeChecker {
isPatternAllowed(
flares: readonly string[],
name: string,
colorPaletteName: string | null,
): PatternResult {
return { type: "unknown" };
}
isCustomFlagAllowed(
flag: string,
flares: readonly string[] | undefined,
): true | "restricted" | "invalid" {
return true;
}
}