chore(release): gate users-branch features for hotfix

Add USERS_BRANCH_FEATURES_ENABLED (off by default) to hide Players library, session tokens, and related UI while keeping the preview import fix. Restore primary Run button when the flag is off.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-04 10:15:28 +08:00
parent 4e6f7321b8
commit cbb6edc378
9 changed files with 148 additions and 59 deletions
+45 -10
View File
@@ -8,6 +8,7 @@ import { installStdoutEpipeGuards } from './safeConsole';
installStdoutEpipeGuards(); installStdoutEpipeGuards();
import { openDialogFilterLabel } from '../shared/appBranding'; import { openDialogFilterLabel } from '../shared/appBranding';
import { USERS_BRANCH_FEATURES_ENABLED } from '../shared/features/usersBranchFeatures';
import { ipcChannels, type ScenePreviewImportEvent, type SessionState } from '../shared/ipc/contracts'; import { ipcChannels, type ScenePreviewImportEvent, type SessionState } from '../shared/ipc/contracts';
import { import {
PROJECT_ZIP_OPEN_DIALOG_FILTER, PROJECT_ZIP_OPEN_DIALOG_FILTER,
@@ -402,8 +403,10 @@ async function main() {
const licenseService = new LicenseService(app.getPath('userData')); const licenseService = new LicenseService(app.getPath('userData'));
tokensStore = new TokensStore(app.getPath('userData')); tokensStore = new TokensStore(app.getPath('userData'));
await tokensStore.ensureLoaded(); await tokensStore.ensureLoaded();
if (USERS_BRANCH_FEATURES_ENABLED) {
playersStore = new PlayersStore(app.getPath('userData')); playersStore = new PlayersStore(app.getPath('userData'));
await playersStore.ensureLoaded(); await playersStore.ensureLoaded();
}
setLicenseAssert(() => { setLicenseAssert(() => {
licenseService.assertForIpc(); licenseService.assertForIpc();
}); });
@@ -429,10 +432,12 @@ async function main() {
sceneTokensSessionStore.reset(); sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset(); sceneNpcTokensSessionStore.reset();
scenePlayerTokensSessionStore.reset(); scenePlayerTokensSessionStore.reset();
if (USERS_BRANCH_FEATURES_ENABLED) {
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : []; const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
if (playerIds.length > 0) { if (playerIds.length > 0) {
scenePlayerTokensSessionStore.dispatch({ kind: 'setSelection', playerIds }); scenePlayerTokensSessionStore.dispatch({ kind: 'setSelection', playerIds });
} }
}
effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() }); effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() });
openMultiWindow(); openMultiWindow();
const project = projectStore.getOpenProject(); const project = projectStore.getOpenProject();
@@ -1357,13 +1362,19 @@ async function main() {
}); });
registerHandler(ipcChannels.players.list, async () => { registerHandler(ipcChannels.players.list, async () => {
await playersStore!.ensureLoaded(); if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { players: playersStore!.listPlayers(), teams: playersStore!.listTeams() }; return { players: [], teams: [] };
}
await playersStore.ensureLoaded();
return { players: playersStore.listPlayers(), teams: playersStore.listTeams() };
}); });
registerHandler( registerHandler(
ipcChannels.players.upsert, ipcChannels.players.upsert,
async ({ id, name, filePath, teamId, ringColor, imageOffset, imageScale }) => { async ({ id, name, filePath, teamId, ringColor, imageOffset, imageScale }) => {
const player = await playersStore!.upsert( if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
throw new Error('Players library is disabled in this build');
}
const player = await playersStore.upsert(
{ {
name, name,
...(id !== undefined ? { id } : {}), ...(id !== undefined ? { id } : {}),
@@ -1380,17 +1391,26 @@ async function main() {
}, },
); );
registerHandler(ipcChannels.players.delete, async ({ id }) => { registerHandler(ipcChannels.players.delete, async ({ id }) => {
await playersStore!.delete(id); if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { ok: true };
}
await playersStore.delete(id);
emitPlayersState(); emitPlayersState();
return { ok: true }; return { ok: true };
}); });
registerHandler(ipcChannels.players.setOrder, async ({ playerIds }) => { registerHandler(ipcChannels.players.setOrder, async ({ playerIds }) => {
const players = await playersStore!.setPlayersOrder(playerIds); if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { players: [] };
}
const players = await playersStore.setPlayersOrder(playerIds);
emitPlayersState(); emitPlayersState();
return { players }; return { players };
}); });
registerHandler(ipcChannels.players.upsertTeam, async ({ id, name, color }) => { registerHandler(ipcChannels.players.upsertTeam, async ({ id, name, color }) => {
const team = await playersStore!.upsertTeam({ if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
throw new Error('Players library is disabled in this build');
}
const team = await playersStore.upsertTeam({
name, name,
...(id !== undefined ? { id } : {}), ...(id !== undefined ? { id } : {}),
...(color !== undefined ? { color } : {}), ...(color !== undefined ? { color } : {}),
@@ -1399,21 +1419,33 @@ async function main() {
return { team }; return { team };
}); });
registerHandler(ipcChannels.players.deleteTeam, async ({ id }) => { registerHandler(ipcChannels.players.deleteTeam, async ({ id }) => {
await playersStore!.deleteTeam(id); if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { ok: true };
}
await playersStore.deleteTeam(id);
emitPlayersState(); emitPlayersState();
return { ok: true }; return { ok: true };
}); });
registerHandler(ipcChannels.players.setTeamsOrder, async ({ teamIds }) => { registerHandler(ipcChannels.players.setTeamsOrder, async ({ teamIds }) => {
const teams = await playersStore!.setTeamsOrder(teamIds); if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { teams: [] };
}
const teams = await playersStore.setTeamsOrder(teamIds);
emitPlayersState(); emitPlayersState();
return { teams }; return { teams };
}); });
registerHandler(ipcChannels.players.assignTeam, async ({ playerId, teamId }) => { registerHandler(ipcChannels.players.assignTeam, async ({ playerId, teamId }) => {
const player = await playersStore!.assignPlayerTeam(playerId, teamId); if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
throw new Error('Players library is disabled in this build');
}
const player = await playersStore.assignPlayerTeam(playerId, teamId);
emitPlayersState(); emitPlayersState();
return { player }; return { player };
}); });
registerHandler(ipcChannels.players.pickImage, async () => { registerHandler(ipcChannels.players.pickImage, async () => {
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { canceled: true as const };
}
const { canceled, filePaths } = await dialog.showOpenDialog({ const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'], properties: ['openFile'],
filters: [ filters: [
@@ -1432,7 +1464,10 @@ async function main() {
return { canceled: false as const, filePath, previewDataUrl }; return { canceled: false as const, filePath, previewDataUrl };
}); });
registerHandler(ipcChannels.players.imageUrl, ({ id }) => { registerHandler(ipcChannels.players.imageUrl, ({ id }) => {
return { url: playersStore!.getImageUrl(id) }; if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { url: null };
}
return { url: playersStore.getImageUrl(id) };
}); });
registerHandler(ipcChannels.sceneNpcTokensSession.getState, () => { registerHandler(ipcChannels.sceneNpcTokensSession.getState, () => {
return { state: sceneNpcTokensSessionStore.getState() }; return { state: sceneNpcTokensSessionStore.getState() };
+2 -2
View File
@@ -81,7 +81,7 @@ async function serveFile(info: ReadInfo, request: Request): Promise<Response> {
export function registerDndAssetProtocol( export function registerDndAssetProtocol(
projectStore: ZipProjectStore, projectStore: ZipProjectStore,
tokensStore: TokensStore, tokensStore: TokensStore,
playersStore: PlayersStore, playersStore: PlayersStore | null,
): void { ): void {
session.defaultSession.protocol.handle('dnd', async (request) => { session.defaultSession.protocol.handle('dnd', async (request) => {
const url = new URL(request.url); const url = new URL(request.url);
@@ -95,7 +95,7 @@ export function registerDndAssetProtocol(
} else if (url.hostname === 'token') { } else if (url.hostname === 'token') {
info = tokensStore.getImageReadInfo(asTokenId(id)); info = tokensStore.getImageReadInfo(asTokenId(id));
} else if (url.hostname === 'player') { } else if (url.hostname === 'player') {
info = playersStore.getImageReadInfo(asPlayerId(id)); info = playersStore?.getImageReadInfo(asPlayerId(id)) ?? null;
} }
if (!info) { if (!info) {
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
+8 -3
View File
@@ -5,6 +5,7 @@ import { pickEraseTargetId } from '../../shared/effectEraserHitTest';
import { fitAspectRect } from '../../shared/geometry/fitAspectRect'; import { fitAspectRect } from '../../shared/geometry/fitAspectRect';
import { ipcChannels } from '../../shared/ipc/contracts'; import { ipcChannels } from '../../shared/ipc/contracts';
import type { SessionState } from '../../shared/ipc/contracts'; import type { SessionState } from '../../shared/ipc/contracts';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import { import {
isNodeInMainStoryline, isNodeInMainStoryline,
isNodeInSideStoryline, isNodeInSideStoryline,
@@ -1778,6 +1779,8 @@ export function ControlApp() {
<div className={styles.previewHeader}> <div className={styles.previewHeader}>
<div className={styles.previewTitle}>{t('control.screenPreview')}</div> <div className={styles.previewTitle}>{t('control.screenPreview')}</div>
<div className={styles.previewActions}> <div className={styles.previewActions}>
{USERS_BRANCH_FEATURES_ENABLED ? (
<>
<label className={styles.npcTokenScale}> <label className={styles.npcTokenScale}>
<span className={styles.npcTokenScaleLabel}>{t('control.npcTokenScale')}</span> <span className={styles.npcTokenScaleLabel}>{t('control.npcTokenScale')}</span>
<input <input
@@ -1816,6 +1819,8 @@ export function ControlApp() {
? t('control.hidePlayers') ? t('control.hidePlayers')
: t('control.showPlayers')} : t('control.showPlayers')}
</Button> </Button>
) : null}
</>
) : null} ) : null}
<Button onClick={() => void api.invoke(ipcChannels.windows.closeMultiWindow, {})}> <Button onClick={() => void api.invoke(ipcChannels.windows.closeMultiWindow, {})}>
{t('control.stopPresentation')} {t('control.stopPresentation')}
@@ -2026,7 +2031,7 @@ export function ControlApp() {
}} }}
/> />
) : null} ) : null}
{previewContentRect ? ( {USERS_BRANCH_FEATURES_ENABLED && previewContentRect ? (
<SceneNpcTokensOverlay <SceneNpcTokensOverlay
placements={currentScene?.npcTokens ?? []} placements={currentScene?.npcTokens ?? []}
library={session?.project?.npcs ?? []} library={session?.project?.npcs ?? []}
@@ -2046,7 +2051,7 @@ export function ControlApp() {
}} }}
/> />
) : null} ) : null}
{previewContentRect ? ( {USERS_BRANCH_FEATURES_ENABLED && previewContentRect ? (
<ScenePlayerTokensOverlay <ScenePlayerTokensOverlay
library={appPlayers} library={appPlayers}
session={scenePlayerTokensSession} session={scenePlayerTokensSession}
@@ -2509,7 +2514,7 @@ export function ControlApp() {
) : null} ) : null}
</div> </div>
{npcSessionCtxMenu {USERS_BRANCH_FEATURES_ENABLED && npcSessionCtxMenu
? createPortal( ? createPortal(
<> <>
<button <button
+22 -1
View File
@@ -2,6 +2,7 @@ import React, { startTransition, useCallback, useEffect, useMemo, useRef, useSta
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder'; import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import type { import type {
NpcImportResolution, NpcImportResolution,
SceneImportResolution, SceneImportResolution,
@@ -841,6 +842,7 @@ export function EditorApp() {
{t('top.file')} {t('top.file')}
</button> </button>
) : null} ) : null}
{USERS_BRANCH_FEATURES_ENABLED ? (
<button <button
type="button" type="button"
data-testid="players-header-btn" data-testid="players-header-btn"
@@ -858,6 +860,7 @@ export function EditorApp() {
> >
{t('top.players')} {t('top.players')}
</button> </button>
) : null}
</div> </div>
<div className={styles.flex1} /> <div className={styles.flex1} />
{appVersionText ? ( {appVersionText ? (
@@ -868,6 +871,7 @@ export function EditorApp() {
<div className={styles.headerActions}> <div className={styles.headerActions}>
{state.project ? ( {state.project ? (
<> <>
{USERS_BRANCH_FEATURES_ENABLED ? (
<div <div
ref={runSplitRef} ref={runSplitRef}
className={styles.splitRun} className={styles.splitRun}
@@ -917,6 +921,19 @@ export function EditorApp() {
</svg> </svg>
</button> </button>
</div> </div>
) : (
<Button
variant="primary"
disabled={runDisabled || launching}
data-testid="run-main-btn"
onClick={() => {
if (!licenseActive || !graphStartGraphNodeId || launching) return;
launchFromGraphNode(graphStartGraphNodeId);
}}
>
{t('top.run')}
</Button>
)}
{runDisabled ? ( {runDisabled ? (
<Button <Button
variant="ghost" variant="ghost"
@@ -1620,6 +1637,8 @@ export function EditorApp() {
}} }}
/> />
<CheckUpdatesModal open={checkUpdatesOpen} onClose={() => setCheckUpdatesOpen(false)} /> <CheckUpdatesModal open={checkUpdatesOpen} onClose={() => setCheckUpdatesOpen(false)} />
{USERS_BRANCH_FEATURES_ENABLED ? (
<>
<PlayersManagerModal open={playersManagerOpen} onClose={() => setPlayersManagerOpen(false)} /> <PlayersManagerModal open={playersManagerOpen} onClose={() => setPlayersManagerOpen(false)} />
<LaunchPlayersModal <LaunchPlayersModal
open={launchPlayersOpen} open={launchPlayersOpen}
@@ -1629,7 +1648,9 @@ export function EditorApp() {
launchFromGraphNode(graphStartGraphNodeId, playerIds); launchFromGraphNode(graphStartGraphNodeId, playerIds);
}} }}
/> />
{runMenuOpen && runMenuPos </>
) : null}
{USERS_BRANCH_FEATURES_ENABLED && runMenuOpen && runMenuPos
? createPortal( ? createPortal(
<div <div
data-runmenu-root="1" data-runmenu-root="1"
+8 -2
View File
@@ -1,5 +1,7 @@
/** Порядок разделов в окне «Инструкция». */ /** Порядок разделов в окне «Инструкция». */
export const HELP_SECTION_IDS = [ import { USERS_BRANCH_FEATURES_ENABLED } from '../../../shared/features/usersBranchFeatures';
const HELP_SECTION_IDS_ALL = [
'overview', 'overview',
'license', 'license',
'projects', 'projects',
@@ -25,7 +27,11 @@ export const HELP_SECTION_IDS = [
'settings', 'settings',
] as const; ] as const;
export type HelpSectionId = (typeof HELP_SECTION_IDS)[number]; export type HelpSectionId = (typeof HELP_SECTION_IDS_ALL)[number];
export const HELP_SECTION_IDS: readonly HelpSectionId[] = USERS_BRANCH_FEATURES_ENABLED
? HELP_SECTION_IDS_ALL
: HELP_SECTION_IDS_ALL.filter((id) => id !== 'players');
export function helpSectionTitleKey(id: HelpSectionId): string { export function helpSectionTitleKey(id: HelpSectionId): string {
return `help.section.${id}.title`; return `help.section.${id}.title`;
+11
View File
@@ -14,6 +14,7 @@ import type {
import editorStyles from '../editor/EditorApp.module.css'; import editorStyles from '../editor/EditorApp.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext'; import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { getDndApi } from '../shared/dndApi'; import { getDndApi } from '../shared/dndApi';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import { PlayerTokenView } from '../shared/playerToken/PlayerTokenView'; import { PlayerTokenView } from '../shared/playerToken/PlayerTokenView';
import { Button, Input, Select } from '../shared/ui/controls'; import { Button, Input, Select } from '../shared/ui/controls';
import { useAssetUrl } from '../shared/useAssetImageUrl'; import { useAssetUrl } from '../shared/useAssetImageUrl';
@@ -737,6 +738,7 @@ export function NpcsEditorApp() {
<div> <div>
<div className={styles.fieldLabel}>{t('npcs.avatar')}</div> <div className={styles.fieldLabel}>{t('npcs.avatar')}</div>
<div className={styles.avatarPick}> <div className={styles.avatarPick}>
{USERS_BRANCH_FEATURES_ENABLED ? (
<PlayerTokenView <PlayerTokenView
name={selected.name} name={selected.name}
imageUrl={selectedUrl} imageUrl={selectedUrl}
@@ -758,6 +760,13 @@ export function NpcsEditorApp() {
}); });
}} }}
/> />
) : (
<div className={styles.avatarPreview}>
{selectedUrl ? (
<img className={styles.avatarPreviewImg} src={selectedUrl} alt="" />
) : null}
</div>
)}
<Button <Button
disabled={avatarBusy} disabled={avatarBusy}
onClick={() => { onClick={() => {
@@ -782,6 +791,7 @@ export function NpcsEditorApp() {
</div> </div>
</div> </div>
{USERS_BRANCH_FEATURES_ENABLED ? (
<label> <label>
<div className={styles.fieldLabel}>{t('npcs.disposition')}</div> <div className={styles.fieldLabel}>{t('npcs.disposition')}</div>
<Select <Select
@@ -800,6 +810,7 @@ export function NpcsEditorApp() {
]} ]}
/> />
</label> </label>
) : null}
<div> <div>
<div className={styles.fieldLabel}>{t('npcs.name')}</div> <div className={styles.fieldLabel}>{t('npcs.name')}</div>
+6 -3
View File
@@ -44,6 +44,7 @@ import {
trapTypeLabelRu, trapTypeLabelRu,
} from '../../shared/types/sceneTraps'; } from '../../shared/types/sceneTraps';
import { sceneViewPanBy, sceneViewZoomAt } from '../../shared/types/sceneView'; import { sceneViewPanBy, sceneViewZoomAt } from '../../shared/types/sceneView';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import editorStyles from '../editor/EditorApp.module.css'; import editorStyles from '../editor/EditorApp.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext'; import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { getDndApi } from '../shared/dndApi'; import { getDndApi } from '../shared/dndApi';
@@ -484,7 +485,7 @@ export function SceneEditorApp() {
return; return;
} }
const npcId = e.dataTransfer.getData(SCENE_NPC_DND_MIME); const npcId = e.dataTransfer.getData(SCENE_NPC_DND_MIME);
if (npcId) { if (USERS_BRANCH_FEATURES_ENABLED && npcId) {
addNpcTokenAt(npcId as NpcId, p.x, p.y); addNpcTokenAt(npcId as NpcId, p.x, p.y);
return; return;
} }
@@ -600,6 +601,7 @@ export function SceneEditorApp() {
) : null} ) : null}
</div> </div>
{USERS_BRANCH_FEATURES_ENABLED ? (
<div className={styles.accordion} data-testid="scene-npc-accordion"> <div className={styles.accordion} data-testid="scene-npc-accordion">
<button type="button" className={styles.accordionHead} onClick={() => setNpcsOpen((v) => !v)}> <button type="button" className={styles.accordionHead} onClick={() => setNpcsOpen((v) => !v)}>
НПС {npcsOpen ? '▾' : '▸'} НПС {npcsOpen ? '▾' : '▸'}
@@ -621,6 +623,7 @@ export function SceneEditorApp() {
</div> </div>
) : null} ) : null}
</div> </div>
) : null}
<div className={styles.accordion}> <div className={styles.accordion}>
<button type="button" className={styles.accordionHead} onClick={() => setTokensOpen((v) => !v)}> <button type="button" className={styles.accordionHead} onClick={() => setTokensOpen((v) => !v)}>
@@ -884,7 +887,7 @@ export function SceneEditorApp() {
); );
}) })
: null} : null}
{contentRect {USERS_BRANCH_FEATURES_ENABLED && contentRect
? localNpcTokens.map((tok) => { ? localNpcTokens.map((tok) => {
const npc = project?.npcs.find((item) => item.id === tok.npcId); const npc = project?.npcs.find((item) => item.id === tok.npcId);
if (!npc) return null; if (!npc) return null;
@@ -1050,7 +1053,7 @@ export function SceneEditorApp() {
) )
: null} : null}
{npcCtxMenu {USERS_BRANCH_FEATURES_ENABLED && npcCtxMenu
? createPortal( ? createPortal(
<> <>
<button <button
+5 -3
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { computeTimeSec } from '../../main/video/videoPlaybackStore'; import { computeTimeSec } from '../../main/video/videoPlaybackStore';
import type { SessionState } from '../../shared/ipc/contracts'; import type { SessionState } from '../../shared/ipc/contracts';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import { ExplosionVideoOverlay } from './effects/ExplosionVideoOverlay'; import { ExplosionVideoOverlay } from './effects/ExplosionVideoOverlay';
import { PixiEffectsOverlay } from './effects/PxiEffectsOverlay'; import { PixiEffectsOverlay } from './effects/PxiEffectsOverlay';
@@ -54,7 +55,8 @@ export function PresentationView({
const [materialsOverlay] = useMaterialsOverlayState(); const [materialsOverlay] = useMaterialsOverlayState();
const [npcsOverlay] = useNpcsOverlayState(); const [npcsOverlay] = useNpcsOverlayState();
const appTokens = useAppTokens(); const appTokens = useAppTokens();
const { players: appPlayers } = useAppPlayers(); const appPlayersResult = useAppPlayers();
const appPlayers = USERS_BRANCH_FEATURES_ENABLED ? appPlayersResult.players : [];
const [sceneTokensSession] = useSceneTokensSession(); const [sceneTokensSession] = useSceneTokensSession();
const [sceneNpcTokensSession] = useSceneNpcTokensSession(); const [sceneNpcTokensSession] = useSceneNpcTokensSession();
const [scenePlayerTokensSession] = useScenePlayerTokensSession(); const [scenePlayerTokensSession] = useScenePlayerTokensSession();
@@ -196,7 +198,7 @@ export function PresentationView({
viewport={contentRect} viewport={contentRect}
/> />
) : null} ) : null}
{scene?.previewAssetType === 'image' && contentRect ? ( {USERS_BRANCH_FEATURES_ENABLED && scene?.previewAssetType === 'image' && contentRect ? (
<SceneNpcTokensOverlay <SceneNpcTokensOverlay
placements={scene.npcTokens ?? []} placements={scene.npcTokens ?? []}
library={project?.npcs ?? []} library={project?.npcs ?? []}
@@ -205,7 +207,7 @@ export function PresentationView({
grid={scene.grid} grid={scene.grid}
/> />
) : null} ) : null}
{scene?.previewAssetType === 'image' && contentRect ? ( {USERS_BRANCH_FEATURES_ENABLED && scene?.previewAssetType === 'image' && contentRect ? (
<ScenePlayerTokensOverlay <ScenePlayerTokensOverlay
library={appPlayers} library={appPlayers}
session={scenePlayerTokensSession} session={scenePlayerTokensSession}
@@ -0,0 +1,6 @@
/**
* Hotfix release: set to `false` to hide everything merged from branch `users`
* (Players library, run-with-players, circular NPC/player session tokens, disposition UI).
* Re-enable after QA.
*/
export const USERS_BRANCH_FEATURES_ENABLED = false;