Move command metadata authority to server
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.03.08 R335";
|
window.CHGRID_WEB_VERSION = "2026.03.08 R336";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -17,6 +17,22 @@ export type ItemPropertyMetadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UiDefinitionsPayload = {
|
type UiDefinitionsPayload = {
|
||||||
|
commandMetadata?: {
|
||||||
|
mainModeActions?: Array<{
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
itemManagement?: {
|
||||||
|
actions?: Array<{
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
anyPermission?: string;
|
||||||
|
ownPermission?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
itemTypeOrder?: ItemType[];
|
itemTypeOrder?: ItemType[];
|
||||||
itemTypes?: Array<{
|
itemTypes?: Array<{
|
||||||
type: ItemType;
|
type: ItemType;
|
||||||
@@ -28,6 +44,17 @@ type UiDefinitionsPayload = {
|
|||||||
globalProperties?: Record<string, unknown>;
|
globalProperties?: Record<string, unknown>;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ServerCommandMetadata = {
|
||||||
|
label?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemManagementActionMetadata = ServerCommandMetadata & {
|
||||||
|
anyPermission?: string;
|
||||||
|
ownPermission?: string;
|
||||||
|
};
|
||||||
|
|
||||||
let itemTypeSequence: ItemType[] = [];
|
let itemTypeSequence: ItemType[] = [];
|
||||||
let itemTypeLabels: Partial<Record<ItemType, string>> = {};
|
let itemTypeLabels: Partial<Record<ItemType, string>> = {};
|
||||||
let itemTypeTooltips: Partial<Record<ItemType, string>> = {};
|
let itemTypeTooltips: Partial<Record<ItemType, string>> = {};
|
||||||
@@ -36,6 +63,8 @@ let itemTypeCapabilities: Partial<Record<ItemType, string[]>> = {};
|
|||||||
let itemTypeGlobalProperties: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
let itemTypeGlobalProperties: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
||||||
let itemTypePropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
let itemTypePropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
||||||
let propertyLabelByKey: Record<string, string> = {};
|
let propertyLabelByKey: Record<string, string> = {};
|
||||||
|
let mainModeCommandMetadataById: Record<string, ServerCommandMetadata> = {};
|
||||||
|
let itemManagementActionMetadataById: Record<string, ItemManagementActionMetadata> = {};
|
||||||
|
|
||||||
export let EDITABLE_ITEM_PROPERTY_KEYS = new Set<string>(
|
export let EDITABLE_ITEM_PROPERTY_KEYS = new Set<string>(
|
||||||
Object.values(itemTypeEditableProperties).flatMap((keys) => keys ?? []),
|
Object.values(itemTypeEditableProperties).flatMap((keys) => keys ?? []),
|
||||||
@@ -150,6 +179,16 @@ export function getItemTypeCapabilities(itemType: ItemType): string[] {
|
|||||||
return [...(itemTypeCapabilities[itemType] ?? [])];
|
return [...(itemTypeCapabilities[itemType] ?? [])];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns server-authored metadata for one server-backed main-mode command id. */
|
||||||
|
export function getServerMainModeCommandMetadata(commandId: string): ServerCommandMetadata | undefined {
|
||||||
|
return mainModeCommandMetadataById[commandId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns server-authored metadata for one item-management action id. */
|
||||||
|
export function getItemManagementActionMetadata(actionId: string): ItemManagementActionMetadata | undefined {
|
||||||
|
return itemManagementActionMetadataById[actionId];
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns human-facing label for a property key. */
|
/** Returns human-facing label for a property key. */
|
||||||
export function itemPropertyLabel(key: string): string {
|
export function itemPropertyLabel(key: string): string {
|
||||||
const metadataLabel = propertyLabelByKey[key];
|
const metadataLabel = propertyLabelByKey[key];
|
||||||
@@ -237,6 +276,8 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
|||||||
itemTypeGlobalProperties = {};
|
itemTypeGlobalProperties = {};
|
||||||
itemTypePropertyMetadata = {};
|
itemTypePropertyMetadata = {};
|
||||||
propertyLabelByKey = {};
|
propertyLabelByKey = {};
|
||||||
|
mainModeCommandMetadataById = {};
|
||||||
|
itemManagementActionMetadataById = {};
|
||||||
rebuildEditablePropertyKeySet();
|
rebuildEditablePropertyKeySet();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -253,6 +294,8 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
|||||||
const nextGlobals: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
const nextGlobals: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
||||||
const nextPropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
const nextPropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
||||||
const nextPropertyLabels: Record<string, string> = {};
|
const nextPropertyLabels: Record<string, string> = {};
|
||||||
|
const nextMainModeCommandMetadata: Record<string, ServerCommandMetadata> = {};
|
||||||
|
const nextItemManagementActionMetadata: Record<string, ItemManagementActionMetadata> = {};
|
||||||
|
|
||||||
for (const definition of uiDefinitions.itemTypes) {
|
for (const definition of uiDefinitions.itemTypes) {
|
||||||
if (!definition || typeof definition.type !== 'string') continue;
|
if (!definition || typeof definition.type !== 'string') continue;
|
||||||
@@ -295,6 +338,28 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
|||||||
discoveredOrder.push(definition.type as ItemType);
|
discoveredOrder.push(definition.type as ItemType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const action of uiDefinitions.commandMetadata?.mainModeActions ?? []) {
|
||||||
|
const id = String(action?.id ?? '').trim();
|
||||||
|
if (!id) continue;
|
||||||
|
nextMainModeCommandMetadata[id] = {
|
||||||
|
label: typeof action?.label === 'string' && action.label.trim().length > 0 ? action.label.trim() : undefined,
|
||||||
|
tooltip: typeof action?.tooltip === 'string' && action.tooltip.trim().length > 0 ? action.tooltip.trim() : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const action of uiDefinitions.itemManagement?.actions ?? []) {
|
||||||
|
const id = String(action?.id ?? '').trim();
|
||||||
|
if (!id) continue;
|
||||||
|
nextItemManagementActionMetadata[id] = {
|
||||||
|
label: typeof action?.label === 'string' && action.label.trim().length > 0 ? action.label.trim() : undefined,
|
||||||
|
tooltip: typeof action?.tooltip === 'string' && action.tooltip.trim().length > 0 ? action.tooltip.trim() : undefined,
|
||||||
|
anyPermission:
|
||||||
|
typeof action?.anyPermission === 'string' && action.anyPermission.trim().length > 0 ? action.anyPermission.trim() : undefined,
|
||||||
|
ownPermission:
|
||||||
|
typeof action?.ownPermission === 'string' && action.ownPermission.trim().length > 0 ? action.ownPermission.trim() : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
itemTypeLabels = nextLabels;
|
itemTypeLabels = nextLabels;
|
||||||
itemTypeTooltips = nextTooltips;
|
itemTypeTooltips = nextTooltips;
|
||||||
itemTypeEditableProperties = nextEditable;
|
itemTypeEditableProperties = nextEditable;
|
||||||
@@ -302,6 +367,8 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
|||||||
itemTypeGlobalProperties = nextGlobals;
|
itemTypeGlobalProperties = nextGlobals;
|
||||||
itemTypePropertyMetadata = nextPropertyMetadata;
|
itemTypePropertyMetadata = nextPropertyMetadata;
|
||||||
propertyLabelByKey = nextPropertyLabels;
|
propertyLabelByKey = nextPropertyLabels;
|
||||||
|
mainModeCommandMetadataById = nextMainModeCommandMetadata;
|
||||||
|
itemManagementActionMetadataById = nextItemManagementActionMetadata;
|
||||||
itemTypeSequence = explicitOrder ?? discoveredOrder;
|
itemTypeSequence = explicitOrder ?? discoveredOrder;
|
||||||
rebuildEditablePropertyKeySet();
|
rebuildEditablePropertyKeySet();
|
||||||
return itemTypeSequence.length > 0;
|
return itemTypeSequence.length > 0;
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import {
|
|||||||
} from './state/gameState';
|
} from './state/gameState';
|
||||||
import {
|
import {
|
||||||
applyServerItemUiDefinitions,
|
applyServerItemUiDefinitions,
|
||||||
|
getItemManagementActionMetadata,
|
||||||
|
getServerMainModeCommandMetadata,
|
||||||
getItemTypeGlobalProperties,
|
getItemTypeGlobalProperties,
|
||||||
getItemTypeSequence,
|
getItemTypeSequence,
|
||||||
getEditableItemPropertyKeys,
|
getEditableItemPropertyKeys,
|
||||||
@@ -192,6 +194,7 @@ type AuthPolicy = {
|
|||||||
type AdminMenuAction = {
|
type AdminMenuAction = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
tooltip?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AdminRoleSummary = {
|
type AdminRoleSummary = {
|
||||||
@@ -220,6 +223,7 @@ type ItemManagementAction = 'delete' | 'transfer';
|
|||||||
type ItemManagementOption = {
|
type ItemManagementOption = {
|
||||||
action: ItemManagementAction;
|
action: ItemManagementAction;
|
||||||
label: string;
|
label: string;
|
||||||
|
tooltip?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ItemManagementConfirmContext = {
|
type ItemManagementConfirmContext = {
|
||||||
@@ -650,11 +654,12 @@ function applyAuthPermissions(role: string | null | undefined, permissions: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Applies server-authored admin menu actions for current session. */
|
/** Applies server-authored admin menu actions for current session. */
|
||||||
function applyServerAdminMenuActions(actions: Array<{ id: string; label: string }> | null | undefined): void {
|
function applyServerAdminMenuActions(actions: Array<{ id: string; label: string; tooltip?: string }> | null | undefined): void {
|
||||||
serverAdminMenuActions = (actions || [])
|
serverAdminMenuActions = (actions || [])
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
id: String(entry.id || '').trim(),
|
id: String(entry.id || '').trim(),
|
||||||
label: String(entry.label || '').trim(),
|
label: String(entry.label || '').trim(),
|
||||||
|
tooltip: typeof entry.tooltip === 'string' && entry.tooltip.trim().length > 0 ? entry.tooltip.trim() : undefined,
|
||||||
}))
|
}))
|
||||||
.filter((entry) => entry.id.length > 0 && entry.label.length > 0);
|
.filter((entry) => entry.id.length > 0 && entry.label.length > 0);
|
||||||
}
|
}
|
||||||
@@ -1064,24 +1069,36 @@ function beginItemSelection(
|
|||||||
|
|
||||||
/** Returns whether the local user can delete the provided item. */
|
/** Returns whether the local user can delete the provided item. */
|
||||||
function canManageDeleteItem(item: WorldItem): boolean {
|
function canManageDeleteItem(item: WorldItem): boolean {
|
||||||
if (hasPermission('item.delete.any')) return true;
|
const metadata = getItemManagementActionMetadata('delete');
|
||||||
return hasPermission('item.delete.own') && authUserId.length > 0 && item.createdBy === authUserId;
|
if (metadata?.anyPermission && hasPermission(metadata.anyPermission)) return true;
|
||||||
|
return Boolean(metadata?.ownPermission) && hasPermission(metadata.ownPermission) && authUserId.length > 0 && item.createdBy === authUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns whether the local user can transfer the provided item. */
|
/** Returns whether the local user can transfer the provided item. */
|
||||||
function canManageTransferItem(item: WorldItem): boolean {
|
function canManageTransferItem(item: WorldItem): boolean {
|
||||||
if (hasPermission('item.transfer.any')) return true;
|
const metadata = getItemManagementActionMetadata('transfer');
|
||||||
return hasPermission('item.transfer.own') && authUserId.length > 0 && item.createdBy === authUserId;
|
if (metadata?.anyPermission && hasPermission(metadata.anyPermission)) return true;
|
||||||
|
return Boolean(metadata?.ownPermission) && hasPermission(metadata.ownPermission) && authUserId.length > 0 && item.createdBy === authUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builds available item-management actions for one selected item. */
|
/** Builds available item-management actions for one selected item. */
|
||||||
function itemManagementOptionsFor(item: WorldItem): ItemManagementOption[] {
|
function itemManagementOptionsFor(item: WorldItem): ItemManagementOption[] {
|
||||||
const options: ItemManagementOption[] = [];
|
const options: ItemManagementOption[] = [];
|
||||||
|
const transferMetadata = getItemManagementActionMetadata('transfer');
|
||||||
if (canManageTransferItem(item) && (state.player.id !== null || state.peers.size > 0)) {
|
if (canManageTransferItem(item) && (state.player.id !== null || state.peers.size > 0)) {
|
||||||
options.push({ action: 'transfer', label: 'Transfer item' });
|
options.push({
|
||||||
|
action: 'transfer',
|
||||||
|
label: transferMetadata?.label ?? 'Transfer item',
|
||||||
|
tooltip: transferMetadata?.tooltip,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
const deleteMetadata = getItemManagementActionMetadata('delete');
|
||||||
if (canManageDeleteItem(item)) {
|
if (canManageDeleteItem(item)) {
|
||||||
options.push({ action: 'delete', label: 'Delete item' });
|
options.push({
|
||||||
|
action: 'delete',
|
||||||
|
label: deleteMetadata?.label ?? 'Delete item',
|
||||||
|
tooltip: deleteMetadata?.tooltip,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
@@ -2570,6 +2587,8 @@ function getAvailableCommandPaletteEntriesForMode(mode: GameMode): Array<Command
|
|||||||
});
|
});
|
||||||
return descriptors.map((descriptor) => ({
|
return descriptors.map((descriptor) => ({
|
||||||
...descriptor,
|
...descriptor,
|
||||||
|
label: getServerMainModeCommandMetadata(descriptor.id)?.label ?? descriptor.label,
|
||||||
|
tooltip: getServerMainModeCommandMetadata(descriptor.id)?.tooltip ?? descriptor.tooltip,
|
||||||
run: mainModeCommandHandlers[descriptor.id],
|
run: mainModeCommandHandlers[descriptor.id],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -3054,6 +3073,11 @@ function handleItemManageOptionsModeInput(code: string, key: string): void {
|
|||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (code === 'Space') {
|
||||||
|
updateStatus(itemManagementOptions[itemManagementOptionIndex]?.tooltip ?? 'No tooltip available.');
|
||||||
|
audio.sfxUiBlip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (control.type === 'select') {
|
if (control.type === 'select') {
|
||||||
const option = itemManagementOptions[itemManagementOptionIndex];
|
const option = itemManagementOptions[itemManagementOptionIndex];
|
||||||
if (option.action === 'delete') {
|
if (option.action === 'delete') {
|
||||||
@@ -3176,6 +3200,11 @@ function handleAdminMenuModeInput(code: string, key: string): void {
|
|||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (code === 'Space') {
|
||||||
|
updateStatus(adminMenuActions[adminMenuIndex]?.tooltip ?? 'No tooltip available.');
|
||||||
|
audio.sfxUiBlip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (control.type === 'select') {
|
if (control.type === 'select') {
|
||||||
const selected = adminMenuActions[adminMenuIndex];
|
const selected = adminMenuActions[adminMenuIndex];
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const actionMetadataSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
tooltip: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const itemSchema = z.object({
|
export const itemSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
type: z.string().min(1),
|
type: z.string().min(1),
|
||||||
@@ -59,7 +65,7 @@ export const welcomeMessageSchema = z.object({
|
|||||||
username: z.string().nullable().optional(),
|
username: z.string().nullable().optional(),
|
||||||
role: z.string().nullable().optional(),
|
role: z.string().nullable().optional(),
|
||||||
permissions: z.array(z.string()).optional(),
|
permissions: z.array(z.string()).optional(),
|
||||||
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
adminMenuActions: z.array(actionMetadataSchema).optional(),
|
||||||
policy: z
|
policy: z
|
||||||
.object({
|
.object({
|
||||||
usernameMinLength: z.number().int().positive(),
|
usernameMinLength: z.number().int().positive(),
|
||||||
@@ -105,7 +111,22 @@ export const welcomeMessageSchema = z.object({
|
|||||||
),
|
),
|
||||||
adminMenu: z
|
adminMenu: z
|
||||||
.object({
|
.object({
|
||||||
actions: z.array(z.object({ id: z.string(), label: z.string() })),
|
actions: z.array(actionMetadataSchema),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
commandMetadata: z
|
||||||
|
.object({
|
||||||
|
mainModeActions: z.array(actionMetadataSchema).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
itemManagement: z
|
||||||
|
.object({
|
||||||
|
actions: z.array(
|
||||||
|
actionMetadataSchema.extend({
|
||||||
|
anyPermission: z.string().optional(),
|
||||||
|
ownPermission: z.string().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
@@ -133,7 +154,7 @@ export const authResultSchema = z.object({
|
|||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
role: z.string().optional(),
|
role: z.string().optional(),
|
||||||
permissions: z.array(z.string()).optional(),
|
permissions: z.array(z.string()).optional(),
|
||||||
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
adminMenuActions: z.array(actionMetadataSchema).optional(),
|
||||||
nickname: z.string().optional(),
|
nickname: z.string().optional(),
|
||||||
authPolicy: z
|
authPolicy: z
|
||||||
.object({
|
.object({
|
||||||
@@ -289,7 +310,7 @@ export const authPermissionsSchema = z.object({
|
|||||||
type: z.literal('auth_permissions'),
|
type: z.literal('auth_permissions'),
|
||||||
role: z.string(),
|
role: z.string(),
|
||||||
permissions: z.array(z.string()),
|
permissions: z.array(z.string()),
|
||||||
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
adminMenuActions: z.array(actionMetadataSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const adminRoleSummarySchema = z.object({
|
const adminRoleSummarySchema = z.object({
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ This document is the authoritative keymap for the client.
|
|||||||
- `Shift+O`: Inspect all item properties
|
- `Shift+O`: Inspect all item properties
|
||||||
- `D`: Pick up/drop item
|
- `D`: Pick up/drop item
|
||||||
- `Z`: Item management menu (delete/transfer when permitted)
|
- `Z`: Item management menu (delete/transfer when permitted)
|
||||||
|
- `Space` in item management menu: Read tooltip/help for the selected action
|
||||||
- `Enter`: Use item
|
- `Enter`: Use item
|
||||||
- `Shift+Enter`: Secondary item action
|
- `Shift+Enter`: Secondary item action
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ Applies to effect select, user/item list modes, item selection, item property li
|
|||||||
## Admin Modes
|
## Admin Modes
|
||||||
|
|
||||||
- `Shift+Z`: Open admin menu
|
- `Shift+Z`: Open admin menu
|
||||||
|
- `Space` on admin root actions: Read tooltip/help for the selected action
|
||||||
- Admin menu options are permission-gated and include:
|
- Admin menu options are permission-gated and include:
|
||||||
- role management
|
- role management
|
||||||
- change user role
|
- change user role
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
- `itemTypes[].editableProperties`: editable property keys by item type
|
- `itemTypes[].editableProperties`: editable property keys by item type
|
||||||
- `itemTypes[].propertyMetadata`: property-level metadata (`valueType`, optional `label`, optional `range`, optional `tooltip`, optional `maxLength`, optional `options`, optional `visibleWhen`)
|
- `itemTypes[].propertyMetadata`: property-level metadata (`valueType`, optional `label`, optional `range`, optional `tooltip`, optional `maxLength`, optional `options`, optional `visibleWhen`)
|
||||||
- `itemTypes[].globalProperties`: non-editable global values (`useSound`, `emitSound`, `useCooldownMs`, `emitRange`, `directional`, `emitSoundSpeed`, `emitSoundTempo`, `emitInitialDelay`, `emitLoopDelay`)
|
- `itemTypes[].globalProperties`: non-editable global values (`useSound`, `emitSound`, `useCooldownMs`, `emitRange`, `directional`, `emitSoundSpeed`, `emitSoundTempo`, `emitInitialDelay`, `emitLoopDelay`)
|
||||||
- `adminMenu.actions`: server-authored admin root menu labels/ordering for the authenticated user.
|
- `commandMetadata.mainModeActions`: server-authored labels/tooltips for server-backed main-mode commands used by the client command palette
|
||||||
|
- `itemManagement.actions`: server-authored labels/tooltips and permission-key metadata for item-management actions (`transfer`, `delete`)
|
||||||
|
- `adminMenu.actions`: server-authored admin root menu labels/tooltips/ordering for the authenticated user
|
||||||
- Client item UI requires this metadata from the server; there is no fallback item definition map.
|
- Client item UI requires this metadata from the server; there is no fallback item definition map.
|
||||||
- Client property help/type rendering is metadata-driven; it does not infer fallback types/tooltips from hardcoded key heuristics.
|
- Client property help/type rendering is metadata-driven; it does not infer fallback types/tooltips from hardcoded key heuristics.
|
||||||
- `visibleWhen` supports equality checks and string negation via `!` prefix (example: `{"mediaEffect": "!off"}`).
|
- `visibleWhen` supports equality checks and string negation via `!` prefix (example: `{"mediaEffect": "!off"}`).
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
- uses `welcome.player` as authoritative starting position (restored from server-side account state when available)
|
- uses `welcome.player` as authoritative starting position (restored from server-side account state when available)
|
||||||
- records `welcome.serverInfo` (`instanceId`, `version`) for restart detection
|
- records `welcome.serverInfo` (`instanceId`, `version`) for restart detection
|
||||||
- if `welcome.serverInfo.version` differs from running client version, auto-reloads the page
|
- if `welcome.serverInfo.version` differs from running client version, auto-reloads the page
|
||||||
- applies `welcome.uiDefinitions` for item menus/properties/options and admin menu labels/order
|
- applies `welcome.uiDefinitions` for item menus/properties/options, server-backed command metadata, item-management metadata, and admin menu labels/order
|
||||||
- sends initial `update_position` echo from server-assigned starting tile
|
- sends initial `update_position` echo from server-assigned starting tile
|
||||||
- sends initial `update_nickname`
|
- sends initial `update_nickname`
|
||||||
- creates peer runtimes for known users
|
- creates peer runtimes for known users
|
||||||
|
|||||||
@@ -134,11 +134,42 @@ AUTH_SESSION_COOKIE_CLIENT_HEADER = "X-Chgrid-Auth-Client"
|
|||||||
AUTH_LOGIN_FAILURE_MESSAGE = "We couldn't log you in. Check your details and try again."
|
AUTH_LOGIN_FAILURE_MESSAGE = "We couldn't log you in. Check your details and try again."
|
||||||
AUTH_RESUME_FAILURE_MESSAGE = "We couldn't restore your session. Please log in again."
|
AUTH_RESUME_FAILURE_MESSAGE = "We couldn't restore your session. Please log in again."
|
||||||
ADMIN_MENU_ACTION_DEFINITIONS: tuple[dict[str, str], ...] = (
|
ADMIN_MENU_ACTION_DEFINITIONS: tuple[dict[str, str], ...] = (
|
||||||
{"id": "manage_roles", "label": "Role management", "permission": "role.manage"},
|
{"id": "manage_roles", "label": "Role management", "tooltip": "Manage roles and their permission sets.", "permission": "role.manage"},
|
||||||
{"id": "change_user_role", "label": "Change user role", "permission": "user.change_role"},
|
{"id": "change_user_role", "label": "Change user role", "tooltip": "Change a user's assigned role.", "permission": "user.change_role"},
|
||||||
{"id": "ban_user", "label": "Ban user", "permission": "user.ban_unban"},
|
{"id": "ban_user", "label": "Ban user", "tooltip": "Disable a user account.", "permission": "user.ban_unban"},
|
||||||
{"id": "unban_user", "label": "Unban user", "permission": "user.ban_unban"},
|
{"id": "unban_user", "label": "Unban user", "tooltip": "Re-enable a disabled user account.", "permission": "user.ban_unban"},
|
||||||
{"id": "delete_account", "label": "Delete account", "permission": "account.delete.any"},
|
{"id": "delete_account", "label": "Delete account", "tooltip": "Delete a user account permanently.", "permission": "account.delete.any"},
|
||||||
|
)
|
||||||
|
|
||||||
|
ITEM_MANAGEMENT_ACTION_DEFINITIONS: tuple[dict[str, str], ...] = (
|
||||||
|
{
|
||||||
|
"id": "transfer",
|
||||||
|
"label": "Transfer item",
|
||||||
|
"tooltip": "Transfer this item to another user.",
|
||||||
|
"anyPermission": "item.transfer.any",
|
||||||
|
"ownPermission": "item.transfer.own",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "delete",
|
||||||
|
"label": "Delete item",
|
||||||
|
"tooltip": "Delete this item from the world.",
|
||||||
|
"anyPermission": "item.delete.any",
|
||||||
|
"ownPermission": "item.delete.own",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
MAIN_MODE_SERVER_COMMAND_DEFINITIONS: tuple[dict[str, str], ...] = (
|
||||||
|
{"id": "addItem", "label": "Add item", "tooltip": "Open the add-item menu."},
|
||||||
|
{"id": "useItem", "label": "Use item", "tooltip": "Use the carried item or a usable item on your current square."},
|
||||||
|
{
|
||||||
|
"id": "secondaryUseItem",
|
||||||
|
"label": "Secondary item action",
|
||||||
|
"tooltip": "Run the secondary action for the carried item or a usable item on your current square.",
|
||||||
|
},
|
||||||
|
{"id": "pickupDropItem", "label": "Pick up or drop item", "tooltip": "Pick up an item or drop your carried item."},
|
||||||
|
{"id": "openItemManagement", "label": "Item management", "tooltip": "Open item management actions for items on your square."},
|
||||||
|
{"id": "editItem", "label": "Edit item properties", "tooltip": "Edit the carried item or an item on your current square."},
|
||||||
|
{"id": "inspectItem", "label": "Inspect item properties", "tooltip": "Inspect all properties for the carried item or an item on your current square."},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -347,7 +378,7 @@ class SignalingServer:
|
|||||||
return []
|
return []
|
||||||
client_permissions = client.permissions or set()
|
client_permissions = client.permissions or set()
|
||||||
return [
|
return [
|
||||||
{"id": action["id"], "label": action["label"]}
|
{"id": action["id"], "label": action["label"], "tooltip": action["tooltip"]}
|
||||||
for action in ADMIN_MENU_ACTION_DEFINITIONS
|
for action in ADMIN_MENU_ACTION_DEFINITIONS
|
||||||
if action["permission"] in client_permissions
|
if action["permission"] in client_permissions
|
||||||
]
|
]
|
||||||
@@ -1695,6 +1726,8 @@ class SignalingServer:
|
|||||||
return {
|
return {
|
||||||
"itemTypeOrder": list(ITEM_TYPE_SEQUENCE),
|
"itemTypeOrder": list(ITEM_TYPE_SEQUENCE),
|
||||||
"itemTypes": item_types,
|
"itemTypes": item_types,
|
||||||
|
"commandMetadata": {"mainModeActions": list(MAIN_MODE_SERVER_COMMAND_DEFINITIONS)},
|
||||||
|
"itemManagement": {"actions": list(ITEM_MANAGEMENT_ACTION_DEFINITIONS)},
|
||||||
"adminMenu": {"actions": self._build_admin_menu_actions_for_client(client)},
|
"adminMenu": {"actions": self._build_admin_menu_actions_for_client(client)},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user