diff --git a/app/game-api/src/router/general/index.ts b/app/game-api/src/router/general/index.ts index d9e2c1c3..6b0984a9 100644 --- a/app/game-api/src/router/general/index.ts +++ b/app/game-api/src/router/general/index.ts @@ -76,12 +76,11 @@ const isFrontStatusPollActive = (poll: FrontStatusPoll, gameTime: CurrentGameTim return started && !ended; }; -const zGeneralSettings = z.object({ +const zGeneralSettings = z.strictObject({ tnmt: z.number().int().optional(), defence_train: z.number().int().optional(), use_treatment: z.number().int().optional(), use_auto_nation_turn: z.number().int().optional(), - use_auto_nation_diplomacy: z.number().int().min(0).max(1).optional(), use_auto_nation_war: z.number().int().min(0).max(1).optional(), use_auto_nation_promotion: z.number().int().min(0).max(1).optional(), use_auto_nation_finance: z.number().int().min(0).max(1).optional(), @@ -244,7 +243,6 @@ const resolveUserSettings = (meta: Record) => { use_auto_nation_turn: readNumber(readSetting('use_auto_nation_turn'), 1), // Ref가 NPC 군주에게만 수행하던 국가 운영은 사용자 군주에게 opt-in이다. // 누락된 값은 신규 게임과 기존 장수 모두 안전한 기본값(사용 안함)으로 해석한다. - use_auto_nation_diplomacy: readNumber(readSetting('use_auto_nation_diplomacy'), 0), use_auto_nation_war: readNumber(readSetting('use_auto_nation_war'), 0), use_auto_nation_promotion: readNumber(readSetting('use_auto_nation_promotion'), 0), use_auto_nation_finance: readNumber(readSetting('use_auto_nation_finance'), 0), diff --git a/app/game-api/test/inGameMenuPermissions.test.ts b/app/game-api/test/inGameMenuPermissions.test.ts index d4320144..d51d9a05 100644 --- a/app/game-api/test/inGameMenuPermissions.test.ts +++ b/app/game-api/test/inGameMenuPermissions.test.ts @@ -584,7 +584,6 @@ describe('in-game my information ownership', () => { defence_train: 80, use_treatment: 21, use_auto_nation_turn: 1, - use_auto_nation_diplomacy: 0, use_auto_nation_war: 0, use_auto_nation_promotion: 0, use_auto_nation_finance: 0, @@ -612,6 +611,18 @@ describe('in-game my information ownership', () => { expect(requestCommand).not.toHaveBeenCalled(); }); + it('rejects the removed automatic diplomacy setting before dispatching it to ENGINE', async () => { + const requestCommand = vi.fn(async () => ({ type: 'setMySetting', ok: true, generalId: 7 })); + const fixture = createContext({ requestCommand }); + + await expect( + appRouter.createCaller(fixture.context).general.setMySetting({ + use_auto_nation_diplomacy: 1, + } as never) + ).rejects.toMatchObject({ code: 'BAD_REQUEST' }); + expect(requestCommand).not.toHaveBeenCalled(); + }); + it('sends settings directly to ENGINE without creating an API input event', async () => { const transaction = vi.fn(async () => { throw new Error('API transaction must not run'); diff --git a/app/game-engine/src/turn/ai/policies.ts b/app/game-engine/src/turn/ai/policies.ts index b87c8af3..206b9f7c 100644 --- a/app/game-engine/src/turn/ai/policies.ts +++ b/app/game-engine/src/turn/ai/policies.ts @@ -61,10 +61,9 @@ export const AVAILABLE_INSTANT_TURN: Record = { NPC전방발령: true, }; -export type UserRulerAutomationFeature = 'diplomacy' | 'war' | 'promotion' | 'finance' | 'capital'; +export type UserRulerAutomationFeature = 'war' | 'promotion' | 'finance' | 'capital'; const USER_RULER_AUTOMATION_META_KEY = { - diplomacy: 'use_auto_nation_diplomacy', war: 'use_auto_nation_war', promotion: 'use_auto_nation_promotion', finance: 'use_auto_nation_finance', @@ -72,7 +71,6 @@ const USER_RULER_AUTOMATION_META_KEY = { } as const satisfies Record; const USER_RULER_ACTION_FEATURE: Readonly> = { - 불가침제의: 'diplomacy', 선전포고: 'war', 천도: 'capital', }; diff --git a/app/game-engine/src/turn/commandRegistry.ts b/app/game-engine/src/turn/commandRegistry.ts index d9b00d3e..2b70fc96 100644 --- a/app/game-engine/src/turn/commandRegistry.ts +++ b/app/game-engine/src/turn/commandRegistry.ts @@ -142,12 +142,11 @@ const zSetMySetting = z.object({ type: z.literal('setMySetting'), userId: z.string().min(1), generalId: zFiniteNumber, - settings: z.object({ + settings: z.strictObject({ tnmt: z.number().int().optional(), defence_train: z.number().int().optional(), use_treatment: z.number().int().optional(), use_auto_nation_turn: z.number().int().optional(), - use_auto_nation_diplomacy: z.number().int().optional(), use_auto_nation_war: z.number().int().optional(), use_auto_nation_promotion: z.number().int().optional(), use_auto_nation_finance: z.number().int().optional(), diff --git a/app/game-engine/src/turn/worldCommandHandler.ts b/app/game-engine/src/turn/worldCommandHandler.ts index 6112f4f5..16ce9125 100644 --- a/app/game-engine/src/turn/worldCommandHandler.ts +++ b/app/game-engine/src/turn/worldCommandHandler.ts @@ -1896,7 +1896,6 @@ async function handleSetMySetting( nextMeta.use_auto_nation_turn = settings.use_auto_nation_turn === 0 ? 0 : 1; } for (const key of [ - 'use_auto_nation_diplomacy', 'use_auto_nation_war', 'use_auto_nation_promotion', 'use_auto_nation_finance', diff --git a/app/game-engine/test/authenticatedActorCommand.test.ts b/app/game-engine/test/authenticatedActorCommand.test.ts index 160fef8d..d7a82780 100644 --- a/app/game-engine/test/authenticatedActorCommand.test.ts +++ b/app/game-engine/test/authenticatedActorCommand.test.ts @@ -98,6 +98,23 @@ describe('authenticated actor-bound command registry and execution', () => { ).toBeNull(); }); + it('rejects the removed automatic diplomacy setting at the daemon boundary', () => { + expect( + normalizeTurnDaemonCommand({ + requestId: 'set-my-setting:removed-diplomacy', + sentAt: '2026-08-28T00:00:00.000Z', + command: { + type: 'setMySetting', + userId: 'user-7', + generalId: 7, + settings: { + use_auto_nation_diplomacy: 1, + }, + } as unknown as TurnDaemonCommand, + }) + ).toBeNull(); + }); + it('rejects every actor-bound queue payload that omits userId', () => { for (const command of buildActorBoundCommands()) { const { diff --git a/app/game-engine/test/generalAiLegacyDecisionParity.test.ts b/app/game-engine/test/generalAiLegacyDecisionParity.test.ts index f9e3ccf3..ef2fa0a4 100644 --- a/app/game-engine/test/generalAiLegacyDecisionParity.test.ts +++ b/app/game-engine/test/generalAiLegacyDecisionParity.test.ts @@ -826,7 +826,7 @@ describe('legacy NPC user-chief promotion parity', () => { expect(run(3, 0, 1)).toEqual([{ generalId: 2, officerLevel: 11, officerCity: 0, permission: 'ambassador' }]); }); - it('keeps user-ruler duties individually disabled until each setting is enabled', () => { + it('keeps non-aggression proposals NPC-only while user-ruler duties remain opt-in', () => { const ruler = makePromotionGeneral({ id: 1, officerLevel: 12, npcState: 0, meta: { killturn: 0 } }); expect(canUseAutomatedNationAction(ruler, '선전포고')).toBe(false); expect(canUseAutomatedNationAction(ruler, '불가침제의')).toBe(false); @@ -839,18 +839,21 @@ describe('legacy NPC user-chief promotion parity', () => { use_auto_nation_capital: 1, use_auto_nation_finance: 1, }; - expect(canUseAutomatedNationAction(ruler, '불가침제의')).toBe(true); + // 기존 DB에 제거된 플래그가 남아 있어도 사용자 군주에게는 다시 활성화되지 않는다. + expect(canUseAutomatedNationAction(ruler, '불가침제의')).toBe(false); expect(canUseAutomatedNationAction(ruler, '선전포고')).toBe(false); expect(canUseAutomatedNationAction(ruler, '천도')).toBe(true); expect(canUseRulerAutomation(ruler, 'finance')).toBe(true); ruler.meta = { ...ruler.meta, - use_auto_nation_diplomacy: 0, use_auto_nation_war: 1, }; expect(canUseAutomatedNationAction(ruler, '불가침제의')).toBe(false); expect(canUseAutomatedNationAction(ruler, '선전포고')).toBe(true); + + const npcRuler = makePromotionGeneral({ id: 2, officerLevel: 12, npcState: 2, meta: { killturn: 0 } }); + expect(canUseAutomatedNationAction(npcRuler, '불가침제의')).toBe(true); }); it('honors the existing automatic nation-turn master switch for user chiefs only', () => { diff --git a/app/game-engine/test/myInformationCommands.test.ts b/app/game-engine/test/myInformationCommands.test.ts index f01f2754..b8d888c8 100644 --- a/app/game-engine/test/myInformationCommands.test.ts +++ b/app/game-engine/test/myInformationCommands.test.ts @@ -237,7 +237,6 @@ describe('my information world commands', () => { defence_train: 94, use_treatment: 200, use_auto_nation_turn: 0, - use_auto_nation_diplomacy: 1, use_auto_nation_war: 1, use_auto_nation_promotion: 1, use_auto_nation_finance: 1, @@ -254,7 +253,6 @@ describe('my information world commands', () => { defence_train: 999, use_treatment: 100, use_auto_nation_turn: 0, - use_auto_nation_diplomacy: 1, use_auto_nation_war: 1, use_auto_nation_promotion: 1, use_auto_nation_finance: 1, diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index 0fb1d9d7..73a765bd 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -276,7 +276,6 @@ const myGeneral = (state: FixtureState) => ({ defence_train: 80, use_treatment: 21, use_auto_nation_turn: 1, - use_auto_nation_diplomacy: 0, use_auto_nation_war: 0, use_auto_nation_promotion: 0, use_auto_nation_finance: 0, @@ -1347,13 +1346,12 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide ]); const rulerAutomation = page.locator('.ruler-automation-settings'); await expect(rulerAutomation).toBeVisible(); - const diplomacyAutomation = page.getByRole('checkbox', { name: '자동 외교 (불가침 제의)' }); + await expect(page.getByRole('checkbox', { name: '자동 외교 (불가침 제의)' })).toHaveCount(0); const warAutomation = page.getByRole('checkbox', { name: '자동 선전포고' }); const promotionAutomation = page.getByRole('checkbox', { name: '자동 수뇌 임명' }); const financeAutomation = page.getByRole('checkbox', { name: '자동 세율·지급률 조정' }); const capitalAutomation = page.getByRole('checkbox', { name: '자동 천도' }); for (const checkbox of [ - diplomacyAutomation, warAutomation, promotionAutomation, financeAutomation, @@ -1421,7 +1419,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide await expect.poll(() => state.settingMutations.length).toBe(1); expect(state.settingMutations[0]).not.toHaveProperty('generalId'); expect(state.settingMutations[0]).toMatchObject({ - use_auto_nation_diplomacy: 1, use_auto_nation_war: 1, use_auto_nation_promotion: 1, use_auto_nation_finance: 1, diff --git a/app/game-frontend/src/views/MyPageView.vue b/app/game-frontend/src/views/MyPageView.vue index dfd8f35f..d56bda4c 100644 --- a/app/game-frontend/src/views/MyPageView.vue +++ b/app/game-frontend/src/views/MyPageView.vue @@ -32,7 +32,6 @@ type SettingForm = { defence_train: number; use_treatment: number; use_auto_nation_turn: number; - use_auto_nation_diplomacy: number; use_auto_nation_war: number; use_auto_nation_promotion: number; use_auto_nation_finance: number; @@ -69,7 +68,6 @@ const form = reactive({ defence_train: 80, use_treatment: 10, use_auto_nation_turn: 1, - use_auto_nation_diplomacy: 0, use_auto_nation_war: 0, use_auto_nation_promotion: 0, use_auto_nation_finance: 0, @@ -430,15 +428,6 @@ onMounted(() => { :disabled="form.use_auto_nation_turn === 0" > 사용자 군주 자동 업무 -