Add skill for gameplay entry type workflows
This commit is contained in:
@@ -9,7 +9,10 @@ import type {
|
||||
AuthUser,
|
||||
PublicUserSummary,
|
||||
} from '../../../packages/shared/src/contracts/auth';
|
||||
import type { ProfileReferralInviteCenterResponse } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type {
|
||||
ProfileReferralInviteCenterResponse,
|
||||
ProfileTaskCenterResponse,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import { AuthUiContext } from '../auth/AuthUiContext';
|
||||
import {
|
||||
RpgEntryHomeView,
|
||||
@@ -19,7 +22,10 @@ import type { PlatformPublicGalleryCard } from './rpgEntryWorldPresentation';
|
||||
|
||||
const {
|
||||
mockBuildReferralCenter,
|
||||
mockBuildTaskCenter,
|
||||
mockClaimRpgProfileTaskReward,
|
||||
mockGetRpgProfileReferralInviteCenter,
|
||||
mockGetRpgProfileTasks,
|
||||
mockGetRpgProfileWalletLedger,
|
||||
mockRedeemRpgProfileReferralInviteCode,
|
||||
} = vi.hoisted(() => {
|
||||
@@ -47,12 +53,73 @@ const {
|
||||
updatedAt: '2026-05-01T08:00:00Z',
|
||||
...overrides,
|
||||
});
|
||||
const buildTaskCenter = (
|
||||
overrides: Partial<ProfileTaskCenterResponse> = {},
|
||||
): ProfileTaskCenterResponse => ({
|
||||
dayKey: 20260503,
|
||||
walletBalance: 0,
|
||||
tasks: [
|
||||
{
|
||||
taskId: 'daily_login',
|
||||
title: '每日登录',
|
||||
description: '',
|
||||
eventKey: 'profile.login.daily',
|
||||
cycle: 'daily',
|
||||
threshold: 1,
|
||||
progressCount: 1,
|
||||
rewardPoints: 10,
|
||||
status: 'claimable',
|
||||
dayKey: 20260503,
|
||||
claimedAt: null,
|
||||
updatedAt: '2026-05-03T08:00:00Z',
|
||||
},
|
||||
],
|
||||
updatedAt: '2026-05-03T08:00:00Z',
|
||||
...overrides,
|
||||
});
|
||||
const buildClaimedTaskCenter = () =>
|
||||
buildTaskCenter({
|
||||
walletBalance: 10,
|
||||
tasks: [
|
||||
{
|
||||
taskId: 'daily_login',
|
||||
title: '每日登录',
|
||||
description: '',
|
||||
eventKey: 'profile.login.daily',
|
||||
cycle: 'daily',
|
||||
threshold: 1,
|
||||
progressCount: 1,
|
||||
rewardPoints: 10,
|
||||
status: 'claimed',
|
||||
dayKey: 20260503,
|
||||
claimedAt: '2026-05-03T08:01:00Z',
|
||||
updatedAt: '2026-05-03T08:01:00Z',
|
||||
},
|
||||
],
|
||||
updatedAt: '2026-05-03T08:01:00Z',
|
||||
});
|
||||
|
||||
return {
|
||||
mockBuildReferralCenter: buildReferralCenter,
|
||||
mockBuildTaskCenter: buildTaskCenter,
|
||||
mockGetRpgProfileReferralInviteCenter: vi.fn(async () =>
|
||||
buildReferralCenter(),
|
||||
),
|
||||
mockGetRpgProfileTasks: vi.fn(async () => buildTaskCenter()),
|
||||
mockClaimRpgProfileTaskReward: vi.fn(async () => ({
|
||||
taskId: 'daily_login',
|
||||
dayKey: 20260503,
|
||||
rewardPoints: 10,
|
||||
walletBalance: 10,
|
||||
ledgerEntry: {
|
||||
id: 'ledger-daily-login',
|
||||
amountDelta: 10,
|
||||
balanceAfter: 10,
|
||||
sourceType: 'daily_task_reward',
|
||||
createdAt: '2026-05-03T08:01:00Z',
|
||||
},
|
||||
center: buildClaimedTaskCenter(),
|
||||
})),
|
||||
mockRedeemRpgProfileReferralInviteCode: vi.fn(async () => ({
|
||||
center: buildReferralCenter({
|
||||
invitedUsers: [],
|
||||
@@ -131,7 +198,9 @@ mockUpdateAuthProfile.mockResolvedValue({
|
||||
|
||||
vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
|
||||
getRpgProfileReferralInviteCenter: mockGetRpgProfileReferralInviteCenter,
|
||||
getRpgProfileTasks: mockGetRpgProfileTasks,
|
||||
getRpgProfileWalletLedger: mockGetRpgProfileWalletLedger,
|
||||
claimRpgProfileTaskReward: mockClaimRpgProfileTaskReward,
|
||||
redeemRpgProfileReferralInviteCode: mockRedeemRpgProfileReferralInviteCode,
|
||||
getRpgProfileRechargeCenter: vi.fn(async () => ({
|
||||
walletBalance: 0,
|
||||
@@ -558,6 +627,40 @@ afterEach(() => {
|
||||
mockGetRpgProfileReferralInviteCenter.mockResolvedValue(
|
||||
mockBuildReferralCenter(),
|
||||
);
|
||||
mockGetRpgProfileTasks.mockResolvedValue(mockBuildTaskCenter());
|
||||
mockClaimRpgProfileTaskReward.mockResolvedValue({
|
||||
taskId: 'daily_login',
|
||||
dayKey: 20260503,
|
||||
rewardPoints: 10,
|
||||
walletBalance: 10,
|
||||
ledgerEntry: {
|
||||
id: 'ledger-daily-login',
|
||||
amountDelta: 10,
|
||||
balanceAfter: 10,
|
||||
sourceType: 'daily_task_reward',
|
||||
createdAt: '2026-05-03T08:01:00Z',
|
||||
},
|
||||
center: mockBuildTaskCenter({
|
||||
walletBalance: 10,
|
||||
tasks: [
|
||||
{
|
||||
taskId: 'daily_login',
|
||||
title: '每日登录',
|
||||
description: '',
|
||||
eventKey: 'profile.login.daily',
|
||||
cycle: 'daily',
|
||||
threshold: 1,
|
||||
progressCount: 1,
|
||||
rewardPoints: 10,
|
||||
status: 'claimed',
|
||||
dayKey: 20260503,
|
||||
claimedAt: '2026-05-03T08:01:00Z',
|
||||
updatedAt: '2026-05-03T08:01:00Z',
|
||||
},
|
||||
],
|
||||
updatedAt: '2026-05-03T08:01:00Z',
|
||||
}),
|
||||
});
|
||||
mockUpdateAuthProfile.mockResolvedValue({
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
@@ -605,6 +708,31 @@ test('opens wallet ledger modal from narrative coin card', async () => {
|
||||
expect(screen.getByText('+30')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('profile daily task shortcut opens task center and claims reward', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRechargeSuccess = vi.fn();
|
||||
|
||||
renderProfileView(onRechargeSuccess);
|
||||
await user.click(screen.getByRole('button', { name: /每日任务/u }));
|
||||
|
||||
expect(await screen.findByText('每日登录')).toBeTruthy();
|
||||
expect(mockGetRpgProfileTasks).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText('1/1')).toBeTruthy();
|
||||
expect(screen.getByText('+10')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '领取' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockClaimRpgProfileTaskReward).toHaveBeenCalledWith('daily_login');
|
||||
});
|
||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(await screen.findByText('已领取 10 光点')).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByRole('button', { name: '已领取' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('profile total play time card always uses hours', () => {
|
||||
renderProfileView(vi.fn(), {
|
||||
totalPlayTimeMs: 90 * 60 * 1000,
|
||||
|
||||
@@ -48,6 +48,8 @@ import type {
|
||||
ProfilePlayStatsResponse,
|
||||
ProfileReferralInviteCenterResponse,
|
||||
ProfileSaveArchiveSummary,
|
||||
ProfileTaskCenterResponse,
|
||||
ProfileTaskItem,
|
||||
ProfileWalletLedgerResponse,
|
||||
RedeemProfileRewardCodeResponse,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
@@ -61,7 +63,9 @@ import {
|
||||
import { copyTextToClipboard } from '../../services/clipboard';
|
||||
import {
|
||||
getRpgProfileReferralInviteCenter,
|
||||
getRpgProfileTasks,
|
||||
getRpgProfileWalletLedger,
|
||||
claimRpgProfileTaskReward,
|
||||
redeemRpgProfileReferralInviteCode,
|
||||
redeemRpgProfileRewardCode,
|
||||
} from '../../services/rpg-entry/rpgProfileClient';
|
||||
@@ -2004,6 +2008,7 @@ const WALLET_LEDGER_SOURCE_LABELS: Record<string, string> = {
|
||||
asset_operation_consume: '资产操作消耗',
|
||||
asset_operation_refund: '资产操作退回',
|
||||
redeem_code_reward: '兑换码奖励',
|
||||
daily_task_reward: '每日任务奖励',
|
||||
};
|
||||
|
||||
function formatWalletLedgerAmount(amountDelta: number) {
|
||||
@@ -2119,6 +2124,142 @@ function WalletLedgerModal({
|
||||
);
|
||||
}
|
||||
|
||||
const PROFILE_TASK_STATUS_LABELS: Record<ProfileTaskItem['status'], string> = {
|
||||
incomplete: '未完成',
|
||||
claimable: '可领取',
|
||||
claimed: '已领取',
|
||||
disabled: '已停用',
|
||||
};
|
||||
|
||||
function ProfileTaskCenterModal({
|
||||
center,
|
||||
isLoading,
|
||||
error,
|
||||
success,
|
||||
claimingTaskId,
|
||||
fallbackBalance,
|
||||
onClose,
|
||||
onRetry,
|
||||
onClaim,
|
||||
}: {
|
||||
center: ProfileTaskCenterResponse | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
success: string | null;
|
||||
claimingTaskId: string | null;
|
||||
fallbackBalance: number;
|
||||
onClose: () => void;
|
||||
onRetry: () => void;
|
||||
onClaim: (taskId: string) => void;
|
||||
}) {
|
||||
const tasks = center?.tasks ?? [];
|
||||
const walletBalance = center?.walletBalance ?? fallbackBalance;
|
||||
|
||||
return (
|
||||
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
|
||||
<div className="platform-recharge-modal w-full max-w-md overflow-hidden rounded-[1.4rem]">
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
||||
<div>
|
||||
<div className="text-base font-black">每日任务</div>
|
||||
<div className="mt-1 text-xs font-semibold text-[var(--platform-text-soft)]">
|
||||
{walletBalance}光点
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭每日任务"
|
||||
onClick={onClose}
|
||||
className="platform-modal-close flex h-9 w-9 items-center justify-center rounded-full"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3 px-5 py-5">
|
||||
{error ? (
|
||||
<div className="platform-profile-error rounded-2xl px-3 py-2 text-xs font-semibold">
|
||||
<div>{error}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRetry}
|
||||
className="platform-primary-button mt-3 rounded-2xl px-4 py-2 text-xs font-black"
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{success ? (
|
||||
<div className="platform-profile-success rounded-2xl px-3 py-2 text-xs font-semibold">
|
||||
{success}
|
||||
</div>
|
||||
) : null}
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-20 animate-pulse rounded-2xl bg-white/10"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : tasks.length === 0 ? (
|
||||
<div className="platform-subpanel rounded-2xl px-4 py-8 text-center text-sm font-semibold text-[var(--platform-text-soft)]">
|
||||
暂无任务
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{tasks.map((task) => {
|
||||
const isClaimable = task.status === 'claimable';
|
||||
const isClaiming = claimingTaskId === task.taskId;
|
||||
const progressLabel = `${Math.min(task.progressCount, task.threshold)}/${task.threshold}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={task.taskId}
|
||||
className="platform-subpanel rounded-2xl px-4 py-4"
|
||||
>
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-base font-black text-[var(--platform-text-strong)]">
|
||||
{task.title}
|
||||
</div>
|
||||
<div className="mt-1 text-xs font-semibold text-[var(--platform-text-soft)]">
|
||||
{progressLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
+{task.rewardPoints}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] font-semibold text-[var(--platform-text-soft)]">
|
||||
{PROFILE_TASK_STATUS_LABELS[task.status]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isClaimable || Boolean(claimingTaskId)}
|
||||
onClick={() => onClaim(task.taskId)}
|
||||
className="platform-primary-button mt-3 w-full rounded-2xl px-4 py-2.5 text-sm font-black disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{isClaiming
|
||||
? '领取中'
|
||||
: task.status === 'claimed'
|
||||
? '已领取'
|
||||
: isClaimable
|
||||
? '领取'
|
||||
: '未完成'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RewardCodeRedeemModal({
|
||||
value,
|
||||
isSubmitting,
|
||||
@@ -2528,6 +2669,14 @@ export function RpgEntryHomeView({
|
||||
null,
|
||||
);
|
||||
const [isLoadingWalletLedger, setIsLoadingWalletLedger] = useState(false);
|
||||
const [isTaskCenterOpen, setIsTaskCenterOpen] = useState(false);
|
||||
const [taskCenter, setTaskCenter] = useState<ProfileTaskCenterResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [taskCenterError, setTaskCenterError] = useState<string | null>(null);
|
||||
const [isLoadingTaskCenter, setIsLoadingTaskCenter] = useState(false);
|
||||
const [claimingTaskId, setClaimingTaskId] = useState<string | null>(null);
|
||||
const [taskClaimSuccess, setTaskClaimSuccess] = useState<string | null>(null);
|
||||
const [profilePopupPanel, setProfilePopupPanel] =
|
||||
useState<ProfilePopupPanel | null>(null);
|
||||
const [referralCenter, setReferralCenter] =
|
||||
@@ -2961,6 +3110,24 @@ export function RpgEntryHomeView({
|
||||
setIsWalletLedgerOpen(true);
|
||||
loadWalletLedger();
|
||||
};
|
||||
const loadTaskCenter = () => {
|
||||
setTaskCenterError(null);
|
||||
setIsLoadingTaskCenter(true);
|
||||
void getRpgProfileTasks()
|
||||
.then(setTaskCenter)
|
||||
.catch((error: unknown) => {
|
||||
setTaskCenter(null);
|
||||
setTaskCenterError(
|
||||
error instanceof Error ? error.message : '读取每日任务失败',
|
||||
);
|
||||
})
|
||||
.finally(() => setIsLoadingTaskCenter(false));
|
||||
};
|
||||
const openTaskCenterPanel = () => {
|
||||
setIsTaskCenterOpen(true);
|
||||
setTaskClaimSuccess(null);
|
||||
loadTaskCenter();
|
||||
};
|
||||
const loadReferralCenter = useCallback(() => {
|
||||
setIsLoadingReferral(true);
|
||||
setIsReferralCenterInitialized(false);
|
||||
@@ -3070,6 +3237,27 @@ export function RpgEntryHomeView({
|
||||
})
|
||||
.finally(() => setIsSubmittingRewardCode(false));
|
||||
};
|
||||
const claimTaskReward = (taskId: string) => {
|
||||
if (claimingTaskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setClaimingTaskId(taskId);
|
||||
setTaskCenterError(null);
|
||||
setTaskClaimSuccess(null);
|
||||
void claimRpgProfileTaskReward(taskId)
|
||||
.then((response) => {
|
||||
setTaskCenter(response.center);
|
||||
setTaskClaimSuccess(`已领取 ${response.rewardPoints} 光点`);
|
||||
void onRechargeSuccess?.();
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
setTaskCenterError(
|
||||
error instanceof Error ? error.message : '领取任务奖励失败',
|
||||
);
|
||||
})
|
||||
.finally(() => setClaimingTaskId(null));
|
||||
};
|
||||
const clearWorkSearch = () => {
|
||||
setActiveWorkSearchKeyword('');
|
||||
setDesktopSearchKeyword('');
|
||||
@@ -3714,6 +3902,17 @@ export function RpgEntryHomeView({
|
||||
aria-label="常用功能"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<ProfileShortcutButton
|
||||
label="每日任务"
|
||||
subLabel={
|
||||
<>
|
||||
<span>领10</span>
|
||||
<Coins className="h-3 w-3" />
|
||||
</>
|
||||
}
|
||||
icon={Star}
|
||||
onClick={openTaskCenterPanel}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="邀请好友"
|
||||
subLabel={
|
||||
@@ -4197,6 +4396,19 @@ export function RpgEntryHomeView({
|
||||
/>
|
||||
) : null}
|
||||
{rewardCodeModal}
|
||||
{isTaskCenterOpen ? (
|
||||
<ProfileTaskCenterModal
|
||||
center={taskCenter}
|
||||
isLoading={isLoadingTaskCenter}
|
||||
error={taskCenterError}
|
||||
success={taskClaimSuccess}
|
||||
claimingTaskId={claimingTaskId}
|
||||
fallbackBalance={remainingNarrativeCoins}
|
||||
onClose={() => setIsTaskCenterOpen(false)}
|
||||
onRetry={loadTaskCenter}
|
||||
onClaim={claimTaskReward}
|
||||
/>
|
||||
) : null}
|
||||
{isProfilePlayStatsOpen ? (
|
||||
<ProfilePlayedWorksModal
|
||||
stats={profilePlayStats}
|
||||
@@ -4303,6 +4515,19 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
</div>
|
||||
{rewardCodeModal}
|
||||
{isTaskCenterOpen ? (
|
||||
<ProfileTaskCenterModal
|
||||
center={taskCenter}
|
||||
isLoading={isLoadingTaskCenter}
|
||||
error={taskCenterError}
|
||||
success={taskClaimSuccess}
|
||||
claimingTaskId={claimingTaskId}
|
||||
fallbackBalance={remainingNarrativeCoins}
|
||||
onClose={() => setIsTaskCenterOpen(false)}
|
||||
onRetry={loadTaskCenter}
|
||||
onClaim={claimTaskReward}
|
||||
/>
|
||||
) : null}
|
||||
{profilePopupPanel ? (
|
||||
<ProfileReferralModal
|
||||
panel={profilePopupPanel}
|
||||
|
||||
@@ -21,4 +21,22 @@ describe('vite dev api proxy', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards the admin route to the admin dev server', async () => {
|
||||
const resolvedConfig =
|
||||
typeof viteConfig === 'function'
|
||||
? await viteConfig({ command: 'serve', mode: 'test' })
|
||||
: viteConfig;
|
||||
|
||||
// 中文注释:本地完整栈需要能从主站 `/admin/` 进入后台,贴近生产同域部署形态。
|
||||
expect(resolvedConfig.server?.proxy).toEqual(
|
||||
expect.objectContaining({
|
||||
'/admin/': expect.objectContaining({
|
||||
target: expect.stringContaining(':3102'),
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
CreateProfileRechargeOrderResponse,
|
||||
ClaimProfileTaskRewardResponse,
|
||||
PlatformBrowseHistoryBatchSyncRequest,
|
||||
PlatformBrowseHistoryResponse,
|
||||
PlatformBrowseHistoryWriteEntry,
|
||||
@@ -9,6 +10,7 @@ import type {
|
||||
ProfileRechargeCenterResponse,
|
||||
ProfileSaveArchiveListResponse,
|
||||
ProfileSaveArchiveResumeResponse,
|
||||
ProfileTaskCenterResponse,
|
||||
ProfileWalletLedgerResponse,
|
||||
RedeemProfileReferralInviteCodeResponse,
|
||||
RedeemProfileRewardCodeResponse,
|
||||
@@ -142,6 +144,27 @@ export function redeemRpgProfileRewardCode(
|
||||
);
|
||||
}
|
||||
|
||||
export function getRpgProfileTasks(options: RuntimeRequestOptions = {}) {
|
||||
return requestRpgRuntimeJson<ProfileTaskCenterResponse>(
|
||||
'/profile/tasks',
|
||||
{ method: 'GET' },
|
||||
'读取每日任务失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function claimRpgProfileTaskReward(
|
||||
taskId: string,
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRpgRuntimeJson<ClaimProfileTaskRewardResponse>(
|
||||
`/profile/tasks/${encodeURIComponent(taskId)}/claim`,
|
||||
{ method: 'POST' },
|
||||
'领取任务奖励失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function getRpgProfilePlayStats(options: RuntimeRequestOptions = {}) {
|
||||
return requestRpgRuntimeJson<ProfilePlayStatsResponse>(
|
||||
'/profile/play-stats',
|
||||
@@ -255,6 +278,8 @@ export const rpgProfileClient = {
|
||||
createRechargeOrder: createRpgProfileRechargeOrder,
|
||||
getReferralInviteCenter: getRpgProfileReferralInviteCenter,
|
||||
redeemReferralInviteCode: redeemRpgProfileReferralInviteCode,
|
||||
getTasks: getRpgProfileTasks,
|
||||
claimTaskReward: claimRpgProfileTaskReward,
|
||||
getSettings: getRpgProfileSettings,
|
||||
putSettings: putRpgProfileSettings,
|
||||
listSaveArchives: listRpgProfileSaveArchives,
|
||||
|
||||
Reference in New Issue
Block a user