perf: stream story assets by page
This commit is contained in:
@@ -63,6 +63,57 @@ export function hasUnitSheetAsset(key: string) {
|
||||
return unitSheetAssetsByKey.has(key);
|
||||
}
|
||||
|
||||
export function unitBaseSheetsReady(scene: Phaser.Scene, keys: Iterable<string>) {
|
||||
return Array.from(keys).every((key) => scene.textures.exists(key));
|
||||
}
|
||||
|
||||
export function waitForUnitBaseSheets(scene: Phaser.Scene, keys: Iterable<string>, onReady: () => void) {
|
||||
waitForUnitBaseSheetsAttempt(scene, Array.from(new Set(keys)), onReady);
|
||||
}
|
||||
|
||||
export function loadUnitBaseSheets(scene: Phaser.Scene, keys: Iterable<string>, onReady: () => void) {
|
||||
const uniqueKeys = Array.from(new Set(keys));
|
||||
if (unitBaseSheetsReady(scene, uniqueKeys)) {
|
||||
onReady();
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingLoads: Array<{ key: string; url: string }> = [];
|
||||
uniqueKeys.forEach((key) => {
|
||||
const sheet = unitSheetAssetsByKey.get(key) ?? missingUnitSheet(key);
|
||||
if (!scene.textures.exists(sheet.key) && !pendingUnitTextureKeys.has(sheet.key)) {
|
||||
pendingUnitTextureKeys.add(sheet.key);
|
||||
pendingLoads.push({ key: sheet.key, url: sheet.url });
|
||||
}
|
||||
});
|
||||
|
||||
if (pendingLoads.length <= 0) {
|
||||
waitForUnitBaseSheets(scene, uniqueKeys, onReady);
|
||||
return;
|
||||
}
|
||||
|
||||
const clearPendingLoads = () => {
|
||||
pendingLoads.forEach(({ key }) => pendingUnitTextureKeys.delete(key));
|
||||
};
|
||||
|
||||
scene.load.once('complete', () => {
|
||||
clearPendingLoads();
|
||||
const missing = uniqueKeys.filter((key) => !scene.textures.exists(key));
|
||||
if (missing.length > 0) {
|
||||
console.warn(`Failed to load unit base sheet textures: ${missing.join(', ')}`);
|
||||
}
|
||||
onReady();
|
||||
});
|
||||
scene.load.once('loaderror', clearPendingLoads);
|
||||
pendingLoads.forEach(({ key, url }) => {
|
||||
scene.load.spritesheet(key, url, {
|
||||
frameWidth: unitSheetFrameSize,
|
||||
frameHeight: unitSheetFrameSize
|
||||
});
|
||||
});
|
||||
scene.load.start();
|
||||
}
|
||||
|
||||
export function loadUnitSheets(scene: Phaser.Scene, keys: Iterable<string>, onReady: () => void) {
|
||||
const uniqueKeys = Array.from(new Set(keys));
|
||||
if (unitSheetsReady(scene, uniqueKeys)) {
|
||||
@@ -110,6 +161,22 @@ function unitSheetsReady(scene: Phaser.Scene, keys: Iterable<string>) {
|
||||
return Array.from(keys).every((key) => scene.textures.exists(key) && scene.textures.exists(`${key}-actions`));
|
||||
}
|
||||
|
||||
function waitForUnitBaseSheetsAttempt(scene: Phaser.Scene, keys: string[], onReady: () => void, attempts = 0) {
|
||||
if (unitBaseSheetsReady(scene, keys)) {
|
||||
onReady();
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempts > 200) {
|
||||
const missing = keys.filter((key) => !scene.textures.exists(key));
|
||||
console.warn(`Timed out waiting for unit base sheet textures: ${missing.join(', ')}`);
|
||||
onReady();
|
||||
return;
|
||||
}
|
||||
|
||||
scene.time.delayedCall(50, () => waitForUnitBaseSheetsAttempt(scene, keys, onReady, attempts + 1));
|
||||
}
|
||||
|
||||
function waitForUnitSheets(scene: Phaser.Scene, keys: string[], onReady: () => void, attempts = 0) {
|
||||
if (unitSheetsReady(scene, keys)) {
|
||||
onReady();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { portraitAssetEntriesForKey, portraitKeyForSpeaker, type PortraitAssetEn
|
||||
import { storyBackgroundAssets, storyBackgroundKeysFor } from '../data/storyAssets';
|
||||
import {
|
||||
ensureUnitAnimations,
|
||||
loadUnitSheets,
|
||||
loadUnitBaseSheets,
|
||||
unitBaseFramesPerDirection,
|
||||
type UnitDirection
|
||||
} from '../data/unitAssets';
|
||||
@@ -28,6 +28,18 @@ type StorySceneData = {
|
||||
nextSceneData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type StoryPageAssetRequest = {
|
||||
pageIndex: number;
|
||||
callbacks: Array<() => void>;
|
||||
interactive: boolean;
|
||||
};
|
||||
|
||||
type StoryPageAssetPlan = {
|
||||
backgrounds: Array<{ key: string; url: string }>;
|
||||
portraits: PortraitAssetEntry[];
|
||||
unitTextureKeys: string[];
|
||||
};
|
||||
|
||||
const sceneShadeDepth = 1;
|
||||
const cutsceneDepth = 8;
|
||||
const dialogDepth = 20;
|
||||
@@ -83,7 +95,14 @@ export class StoryScene extends Phaser.Scene {
|
||||
private progressBackground?: Phaser.GameObjects.Rectangle;
|
||||
private progressText?: Phaser.GameObjects.Text;
|
||||
private loadingText?: Phaser.GameObjects.Text;
|
||||
private pageLoadingText?: Phaser.GameObjects.Text;
|
||||
private cutsceneLayer?: Phaser.GameObjects.Container;
|
||||
private readyStoryPageIndexes = new Set<number>();
|
||||
private storyPageAssetQueue: StoryPageAssetRequest[] = [];
|
||||
private activeStoryPageAssetRequest?: StoryPageAssetRequest;
|
||||
private activeStoryLoadErrorHandler?: (file: Phaser.Loader.File) => void;
|
||||
private storyAssetLoadGeneration = 0;
|
||||
private storyAssetFailures = new Set<string>();
|
||||
|
||||
constructor() {
|
||||
super('StoryScene');
|
||||
@@ -95,6 +114,12 @@ export class StoryScene extends Phaser.Scene {
|
||||
this.nextSceneData = data?.nextSceneData;
|
||||
this.pageIndex = 0;
|
||||
this.transitioning = false;
|
||||
this.readyStoryPageIndexes.clear();
|
||||
this.storyPageAssetQueue = [];
|
||||
this.activeStoryPageAssetRequest = undefined;
|
||||
this.activeStoryLoadErrorHandler = undefined;
|
||||
this.storyAssetFailures.clear();
|
||||
this.storyAssetLoadGeneration += 1;
|
||||
}
|
||||
|
||||
getDebugState() {
|
||||
@@ -134,7 +159,22 @@ export class StoryScene extends Phaser.Scene {
|
||||
advanceLabel: this.progressActionLabel(isLastPage),
|
||||
nextScene: this.nextScene,
|
||||
cutsceneKind: page?.cutscene?.kind ?? null,
|
||||
cutsceneActors: page?.cutscene?.actors?.map((actor) => actor.unitId) ?? []
|
||||
cutsceneActors: page?.cutscene?.actors?.map((actor) => actor.unitId) ?? [],
|
||||
assetStreaming: {
|
||||
currentPageReady: this.readyStoryPageIndexes.has(this.pageIndex),
|
||||
nextPageIndex: this.pageIndex + 1 < this.pages.length ? this.pageIndex + 1 : null,
|
||||
nextPageReady: this.pageIndex + 1 < this.pages.length ? this.readyStoryPageIndexes.has(this.pageIndex + 1) : null,
|
||||
loadingPageIndex: this.activeStoryPageAssetRequest?.pageIndex ?? null,
|
||||
queuedPageIndexes: this.storyPageAssetQueue.map((request) => request.pageIndex),
|
||||
readyPageIndexes: [...this.readyStoryPageIndexes].sort((left, right) => left - right),
|
||||
failedAssetKeys: [...this.storyAssetFailures],
|
||||
currentUnitBaseTexturesLoaded: page
|
||||
? this.storyPageAssetPlan(this.pageIndex).unitTextureKeys.filter((key) => this.textures.exists(key))
|
||||
: [],
|
||||
currentUnitActionTexturesLoaded: page
|
||||
? this.storyPageAssetPlan(this.pageIndex).unitTextureKeys.filter((key) => this.textures.exists(`${key}-actions`))
|
||||
: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -151,7 +191,22 @@ export class StoryScene extends Phaser.Scene {
|
||||
});
|
||||
this.loadingText.setOrigin(0.5);
|
||||
|
||||
this.ensureStoryAssetsLoaded(() => {
|
||||
const generation = this.storyAssetLoadGeneration;
|
||||
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
|
||||
this.storyAssetLoadGeneration += 1;
|
||||
this.storyPageAssetQueue = [];
|
||||
this.activeStoryPageAssetRequest = undefined;
|
||||
if (this.activeStoryLoadErrorHandler) {
|
||||
this.load.off('loaderror', this.activeStoryLoadErrorHandler);
|
||||
this.activeStoryLoadErrorHandler = undefined;
|
||||
}
|
||||
this.pageLoadingText = undefined;
|
||||
});
|
||||
|
||||
this.ensureStoryPageAssets(0, () => {
|
||||
if (generation !== this.storyAssetLoadGeneration) {
|
||||
return;
|
||||
}
|
||||
this.loadingText?.destroy();
|
||||
this.loadingText = undefined;
|
||||
this.background = this.add.image(width / 2, height / 2, this.resolveBackgroundKey(this.pageBackgroundKey(this.pages[0])));
|
||||
@@ -163,57 +218,195 @@ export class StoryScene extends Phaser.Scene {
|
||||
this.input.on('pointerdown', () => this.advance());
|
||||
this.input.keyboard?.on('keydown-SPACE', () => this.advance());
|
||||
this.input.keyboard?.on('keydown-ENTER', () => this.advance());
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
private ensureStoryAssetsLoaded(onReady: () => void) {
|
||||
const requestedKeys = this.pages.flatMap((page) => storyBackgroundKeysFor(page.background));
|
||||
const requestedUnitTextureKeys = Array.from(new Set(this.pages.flatMap((page) => storyCutsceneActorTextureKeys(page.cutscene))));
|
||||
const missingKeys = Array.from(new Set(requestedKeys)).filter((key) => !this.textures.exists(key));
|
||||
const loadableKeys = missingKeys.filter((key) => storyBackgroundAssets[key]);
|
||||
const missingPortraits = this.pages
|
||||
.flatMap((page) => {
|
||||
const cutscenePortraits =
|
||||
page.cutscene?.actors?.flatMap((actor) => {
|
||||
const profile = storyCutsceneActorProfileFor(actor.unitId);
|
||||
return profile ? portraitAssetEntriesForKey(profile.portraitKey) : [];
|
||||
}) ?? [];
|
||||
const entry = this.pagePortraitAssetEntry(page);
|
||||
return entry ? [entry, ...cutscenePortraits] : cutscenePortraits;
|
||||
})
|
||||
.filter((entry, index, entries) => entries.findIndex((candidate) => candidate.textureKey === entry.textureKey) === index)
|
||||
.filter((entry) => !this.textures.exists(entry.textureKey));
|
||||
|
||||
missingKeys
|
||||
.filter((key) => !storyBackgroundAssets[key])
|
||||
.forEach((key) => console.warn(`Missing story background asset for ${key}`));
|
||||
|
||||
const finishLoading = () => {
|
||||
if (requestedUnitTextureKeys.length === 0) {
|
||||
onReady();
|
||||
return;
|
||||
}
|
||||
|
||||
loadUnitSheets(this, requestedUnitTextureKeys, () => {
|
||||
ensureUnitAnimations(this, requestedUnitTextureKeys);
|
||||
onReady();
|
||||
});
|
||||
};
|
||||
|
||||
if (loadableKeys.length === 0 && missingPortraits.length === 0) {
|
||||
finishLoading();
|
||||
private ensureStoryPageAssets(pageIndex: number, onReady?: () => void, interactive = false) {
|
||||
if (pageIndex < 0 || pageIndex >= this.pages.length) {
|
||||
onReady?.();
|
||||
return;
|
||||
}
|
||||
|
||||
this.load.once('complete', finishLoading);
|
||||
this.load.once('loaderror', (file: Phaser.Loader.File) => {
|
||||
console.warn(`Failed to load story background ${file.key}`);
|
||||
if (this.readyStoryPageIndexes.has(pageIndex)) {
|
||||
onReady?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const activeRequest = this.activeStoryPageAssetRequest;
|
||||
if (activeRequest?.pageIndex === pageIndex) {
|
||||
if (onReady) {
|
||||
activeRequest.callbacks.push(onReady);
|
||||
}
|
||||
activeRequest.interactive ||= interactive;
|
||||
this.refreshPageLoadingIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
const queuedRequest = this.storyPageAssetQueue.find((request) => request.pageIndex === pageIndex);
|
||||
if (queuedRequest) {
|
||||
if (onReady) {
|
||||
queuedRequest.callbacks.push(onReady);
|
||||
}
|
||||
queuedRequest.interactive ||= interactive;
|
||||
if (interactive) {
|
||||
this.storyPageAssetQueue = [
|
||||
queuedRequest,
|
||||
...this.storyPageAssetQueue.filter((request) => request !== queuedRequest)
|
||||
];
|
||||
}
|
||||
this.refreshPageLoadingIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
const request: StoryPageAssetRequest = {
|
||||
pageIndex,
|
||||
callbacks: onReady ? [onReady] : [],
|
||||
interactive
|
||||
};
|
||||
if (interactive) {
|
||||
this.storyPageAssetQueue.unshift(request);
|
||||
} else {
|
||||
this.storyPageAssetQueue.push(request);
|
||||
}
|
||||
this.processStoryPageAssetQueue();
|
||||
}
|
||||
|
||||
private processStoryPageAssetQueue() {
|
||||
if (this.activeStoryPageAssetRequest || this.storyPageAssetQueue.length === 0) {
|
||||
this.refreshPageLoadingIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
const request = this.storyPageAssetQueue.shift();
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeStoryPageAssetRequest = request;
|
||||
this.refreshPageLoadingIndicator();
|
||||
const generation = this.storyAssetLoadGeneration;
|
||||
const plan = this.storyPageAssetPlan(request.pageIndex);
|
||||
|
||||
const finishRequest = () => {
|
||||
if (generation !== this.storyAssetLoadGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureUnitAnimations(this, plan.unitTextureKeys);
|
||||
this.readyStoryPageIndexes.add(request.pageIndex);
|
||||
this.activeStoryPageAssetRequest = undefined;
|
||||
this.refreshPageLoadingIndicator();
|
||||
request.callbacks.forEach((callback) => callback());
|
||||
this.processStoryPageAssetQueue();
|
||||
};
|
||||
|
||||
const loadUnitBases = () => {
|
||||
if (generation !== this.storyAssetLoadGeneration || plan.unitTextureKeys.length === 0) {
|
||||
finishRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loadUnitBaseSheets(this, plan.unitTextureKeys, finishRequest);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to prepare story unit bases for page ${request.pageIndex}`, error);
|
||||
plan.unitTextureKeys
|
||||
.filter((key) => !this.textures.exists(key))
|
||||
.forEach((key) => this.storyAssetFailures.add(key));
|
||||
finishRequest();
|
||||
}
|
||||
};
|
||||
|
||||
const imageLoads = [
|
||||
...plan.backgrounds.map(({ key, url }) => ({ key, url })),
|
||||
...plan.portraits.map(({ textureKey: key, url }) => ({ key, url }))
|
||||
].filter(({ key }, index, entries) => {
|
||||
return !this.textures.exists(key) && entries.findIndex((entry) => entry.key === key) === index;
|
||||
});
|
||||
loadableKeys.forEach((key) => this.load.image(key, storyBackgroundAssets[key]));
|
||||
missingPortraits.forEach(({ textureKey, url }) => this.load.image(textureKey, url));
|
||||
|
||||
if (imageLoads.length === 0) {
|
||||
loadUnitBases();
|
||||
return;
|
||||
}
|
||||
|
||||
const onLoadError = (file: Phaser.Loader.File) => {
|
||||
this.storyAssetFailures.add(String(file.key));
|
||||
console.warn(`Failed to load story asset ${file.key}`);
|
||||
};
|
||||
this.activeStoryLoadErrorHandler = onLoadError;
|
||||
this.load.on('loaderror', onLoadError);
|
||||
this.load.once('complete', () => {
|
||||
this.load.off('loaderror', onLoadError);
|
||||
if (this.activeStoryLoadErrorHandler === onLoadError) {
|
||||
this.activeStoryLoadErrorHandler = undefined;
|
||||
}
|
||||
loadUnitBases();
|
||||
});
|
||||
imageLoads.forEach(({ key, url }) => this.load.image(key, url));
|
||||
this.load.start();
|
||||
}
|
||||
|
||||
private storyPageAssetPlan(pageIndex: number): StoryPageAssetPlan {
|
||||
const page = this.pages[pageIndex];
|
||||
if (!page) {
|
||||
return { backgrounds: [], portraits: [], unitTextureKeys: [] };
|
||||
}
|
||||
|
||||
const backgroundKey = this.pageBackgroundAssetKey(page);
|
||||
const backgroundUrl = backgroundKey ? storyBackgroundAssets[backgroundKey] : undefined;
|
||||
if (backgroundKey && !backgroundUrl) {
|
||||
this.storyAssetFailures.add(backgroundKey);
|
||||
console.warn(`Missing story background asset for ${backgroundKey}`);
|
||||
}
|
||||
|
||||
const pagePortrait = this.pagePortraitAssetEntry(page);
|
||||
const actorPortraits =
|
||||
page.cutscene?.actors
|
||||
?.map((actor, index) => this.actorPortraitAssetEntry(actor, index))
|
||||
.filter((entry): entry is PortraitAssetEntry => entry !== undefined) ?? [];
|
||||
const portraits = [pagePortrait, ...actorPortraits]
|
||||
.filter((entry): entry is PortraitAssetEntry => entry !== undefined)
|
||||
.filter((entry, index, entries) => entries.findIndex((candidate) => candidate.textureKey === entry.textureKey) === index);
|
||||
|
||||
return {
|
||||
backgrounds: backgroundKey && backgroundUrl ? [{ key: backgroundKey, url: backgroundUrl }] : [],
|
||||
portraits,
|
||||
unitTextureKeys: Array.from(new Set(storyCutsceneActorTextureKeys(page.cutscene)))
|
||||
};
|
||||
}
|
||||
|
||||
private prefetchNextStoryPage(pageIndex: number) {
|
||||
const nextPageIndex = pageIndex + 1;
|
||||
if (nextPageIndex < this.pages.length) {
|
||||
this.ensureStoryPageAssets(nextPageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private refreshPageLoadingIndicator() {
|
||||
const interactiveRequest =
|
||||
(this.activeStoryPageAssetRequest?.interactive ? this.activeStoryPageAssetRequest : undefined) ??
|
||||
this.storyPageAssetQueue.find((request) => request.interactive);
|
||||
|
||||
if (!interactiveRequest || this.loadingText) {
|
||||
this.pageLoadingText?.destroy();
|
||||
this.pageLoadingText = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pageLoadingText) {
|
||||
this.pageLoadingText = this.add.text(this.scale.width / 2, this.scale.height - ui(54), '', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: uiPx(14),
|
||||
color: '#f5e6b8',
|
||||
backgroundColor: '#070809',
|
||||
padding: { x: ui(18), y: ui(8) }
|
||||
});
|
||||
this.pageLoadingText.setOrigin(0.5);
|
||||
this.pageLoadingText.setDepth(dialogDepth + 6);
|
||||
}
|
||||
this.pageLoadingText.setText(`다음 장면 준비 중… ${interactiveRequest.pageIndex + 1} / ${this.pages.length}`);
|
||||
}
|
||||
|
||||
private createFallbackStoryTexture() {
|
||||
if (this.textures.exists('story-fallback')) {
|
||||
return;
|
||||
@@ -344,6 +537,7 @@ export class StoryScene extends Phaser.Scene {
|
||||
|
||||
if (immediate) {
|
||||
this.applyPage(page);
|
||||
this.prefetchNextStoryPage(index);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -361,7 +555,8 @@ export class StoryScene extends Phaser.Scene {
|
||||
duration: 120,
|
||||
ease: 'Sine.easeIn',
|
||||
onComplete: () => {
|
||||
this.applyPage(page);
|
||||
this.applyPage(page);
|
||||
this.prefetchNextStoryPage(index);
|
||||
this.background?.setAlpha(0.68);
|
||||
this.setDialogueAlpha(0.18);
|
||||
this.tweens.add({
|
||||
@@ -454,7 +649,11 @@ export class StoryScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private pageBackgroundKey(page: StoryPage) {
|
||||
const keys = storyBackgroundKeysFor(page.background).filter((key) => this.textures.exists(key));
|
||||
return this.pageBackgroundAssetKey(page) ?? page.background;
|
||||
}
|
||||
|
||||
private pageBackgroundAssetKey(page: StoryPage) {
|
||||
const keys = storyBackgroundKeysFor(page.background).filter((key) => Boolean(storyBackgroundAssets[key]));
|
||||
if (keys.length <= 1) {
|
||||
return keys[0] ?? page.background;
|
||||
}
|
||||
@@ -1058,17 +1257,22 @@ export class StoryScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private actorPortraitTextureKey(actor: StoryCutsceneActor, index: number) {
|
||||
const entry = this.actorPortraitAssetEntry(actor, index);
|
||||
return entry && this.textures.exists(entry.textureKey) ? entry.textureKey : undefined;
|
||||
}
|
||||
|
||||
private actorPortraitAssetEntry(actor: StoryCutsceneActor, index: number) {
|
||||
const profile = storyCutsceneActorProfileFor(actor.unitId);
|
||||
if (!profile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const entries = portraitAssetEntriesForKey(profile.portraitKey).filter((entry) => this.textures.exists(entry.textureKey));
|
||||
const entries = portraitAssetEntriesForKey(profile.portraitKey);
|
||||
if (entries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entries[(this.hashString(`${actor.unitId}-${index}`) % entries.length)].textureKey;
|
||||
return entries[this.hashString(`${actor.unitId}-${index}`) % entries.length];
|
||||
}
|
||||
|
||||
private storyUnitFrameIndex(direction: UnitDirection, frame = 0) {
|
||||
@@ -1101,8 +1305,19 @@ export class StoryScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pageIndex += 1;
|
||||
this.showPage(this.pageIndex);
|
||||
const targetPageIndex = this.pageIndex + 1;
|
||||
this.transitioning = true;
|
||||
this.ensureStoryPageAssets(
|
||||
targetPageIndex,
|
||||
() => {
|
||||
if (!this.scene.isActive()) {
|
||||
return;
|
||||
}
|
||||
this.pageIndex = targetPageIndex;
|
||||
this.showPage(targetPageIndex);
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private dialogueObjects() {
|
||||
|
||||
Reference in New Issue
Block a user