mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 21:24:14 +00:00
a26585a47b
## 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
157 lines
4.3 KiB
TypeScript
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;
|
|
}
|
|
}
|