diff --git a/apps/api/src/modules/config/performance-service.ts b/apps/api/src/modules/config/performance-service.ts index cee9f28..dc5089d 100644 --- a/apps/api/src/modules/config/performance-service.ts +++ b/apps/api/src/modules/config/performance-service.ts @@ -21,6 +21,7 @@ const FIELD_LOCATIONS: Record< disableThirdPerson: ['gameProperties', 'disableThirdPerson'], fastValidation: ['gameProperties', 'fastValidation'], battlEye: ['gameProperties', 'battlEye'], + disableAI: ['operating', 'disableAI'], aiLimit: ['operating', 'aiLimit'], playerSaveTime: ['operating', 'playerSaveTime'], slotReservationTimeout: ['operating', 'slotReservationTimeout'], diff --git a/apps/api/src/modules/config/reforger-config-file.test.ts b/apps/api/src/modules/config/reforger-config-file.test.ts index 0661f95..6c4b74c 100644 --- a/apps/api/src/modules/config/reforger-config-file.test.ts +++ b/apps/api/src/modules/config/reforger-config-file.test.ts @@ -11,7 +11,7 @@ const REAL_SHAPE = { a2s: { address: '0.0.0.0', port: 17777 }, rcon: { address: '127.0.0.1', port: 19999, password: 'hunter2', permission: 'admin' }, game: { - name: 'DazzledCorp Training Grounds', + name: 'DZR Training Grounds', password: '', passwordAdmin: 'secret', admins: ['76561198000000000'], @@ -34,7 +34,12 @@ const REAL_SHAPE = { { modId: '5AAF0CCE3F001FB5' }, ], }, - operating: { lobbyPlayerSynchronise: true, aiLimit: -1, playerSaveTime: 120 }, + operating: { + lobbyPlayerSynchronise: true, + disableAI: false, + aiLimit: -1, + playerSaveTime: 120 + }, }; describe('parseReforgerConfigJson', () => { @@ -44,6 +49,7 @@ describe('parseReforgerConfigJson', () => { serverName: 'DazzledCorp Training Grounds', maxPlayers: 16, scenarioId: '{ECC61978EDCC2B5A}Missions/23_Campaign.conf', + disableAI: false, aiLimit: -1, serverMaxViewDistance: 2500, networkViewDistance: 1000, @@ -66,6 +72,7 @@ describe('parseReforgerConfigJson', () => { const config = parseReforgerConfigJson('{"game":{"name":"Bare"}}'); expect(config.serverName).toBe('Bare'); expect(config.maxPlayers).toBe(0); + expect(config.disableAI).toBe(false); expect(config.aiLimit).toBe(-1); expect(config.mods).toEqual([]); }); diff --git a/apps/api/src/modules/config/reforger-config-file.ts b/apps/api/src/modules/config/reforger-config-file.ts index d0d7365..b74138d 100644 --- a/apps/api/src/modules/config/reforger-config-file.ts +++ b/apps/api/src/modules/config/reforger-config-file.ts @@ -52,6 +52,7 @@ export function mapReforgerConfig(raw: unknown): ReforgerServerConfig { serverName: str(game.name, 'Unnamed server'), maxPlayers: num(game.maxPlayers, 0), scenarioId: str(game.scenarioId), + disableAI: bool(operating.disableAI, false), // -1 means "no limit" in Reforger's operating.aiLimit. aiLimit: num(operating.aiLimit, -1), serverMaxViewDistance: num(gameProperties.serverMaxViewDistance, 0), diff --git a/apps/api/src/modules/pterodactyl/mock-provider.ts b/apps/api/src/modules/pterodactyl/mock-provider.ts index 22b04cd..80c0237 100644 --- a/apps/api/src/modules/pterodactyl/mock-provider.ts +++ b/apps/api/src/modules/pterodactyl/mock-provider.ts @@ -126,7 +126,7 @@ export class MockGameServerProvider implements GameServerProvider { }, mods: [{ modId: '591AF5BDA9F7CE8B', name: 'Mock Sample Mod', version: '1.0.2' }], }, - operating: { aiLimit: 40 }, + operating: { disableAI: false, aiLimit: 40 }, }, null, 2, diff --git a/apps/api/src/modules/servers/server-routes.ts b/apps/api/src/modules/servers/server-routes.ts index c1c68c4..ebe1da1 100644 --- a/apps/api/src/modules/servers/server-routes.ts +++ b/apps/api/src/modules/servers/server-routes.ts @@ -64,6 +64,7 @@ const performanceBodySchema = z disableThirdPerson: z.boolean().nullable(), fastValidation: z.boolean().nullable(), battlEye: z.boolean().nullable(), + disableAI: z.boolean().nullable(), aiLimit: z.number().int().min(-1).max(1000).nullable(), playerSaveTime: z.number().int().min(1).max(3600).nullable(), slotReservationTimeout: z.number().int().min(5).max(300).nullable(), diff --git a/apps/api/test/auth-routes.test.ts b/apps/api/test/auth-routes.test.ts index 3051d79..36f090f 100644 --- a/apps/api/test/auth-routes.test.ts +++ b/apps/api/test/auth-routes.test.ts @@ -343,6 +343,7 @@ describe('performance config by role', () => { disableThirdPerson: null, fastValidation: null, battlEye: null, + disableAI: null, aiLimit: null, playerSaveTime: null, slotReservationTimeout: null, diff --git a/apps/api/test/mods-service.test.ts b/apps/api/test/mods-service.test.ts index 057404c..506e24b 100644 --- a/apps/api/test/mods-service.test.ts +++ b/apps/api/test/mods-service.test.ts @@ -69,6 +69,7 @@ describe('ServerModsService', () => { expect(parsed.bindPort).toBe(2001); expect(parsed.game.name).toBe('Mock Reforger Server'); expect(parsed.game.maxPlayers).toBe(16); + expect(parsed.operating.disableAI).toBe(false); expect(parsed.operating.aiLimit).toBe(40); }); diff --git a/apps/api/test/performance-service.test.ts b/apps/api/test/performance-service.test.ts index a0f17d6..bd0e32a 100644 --- a/apps/api/test/performance-service.test.ts +++ b/apps/api/test/performance-service.test.ts @@ -37,6 +37,7 @@ describe('PerformanceSettingsService', () => { // Present in the mock config.json: expect(settings.maxPlayers).toBe(16); expect(settings.serverMaxViewDistance).toBe(2500); + expect(settings.disableAI).toBe(false); expect(settings.aiLimit).toBe(40); expect(settings.disableThirdPerson).toBe(false); // Absent keys: @@ -50,10 +51,11 @@ describe('PerformanceSettingsService', () => { ...settings, maxPlayers: 32, playerSaveTime: 180, // new key + disableAI: null, aiLimit: null, // remove key → game default }); - expect(result.changedFields.sort()).toEqual(['aiLimit', 'maxPlayers', 'playerSaveTime']); + expect(result.changedFields.sort()).toEqual(['aiLimit', 'disableAI', 'maxPlayers', 'playerSaveTime']); expect(result.requiresRestart).toBe(true); const written = JSON.parse(provider.writtenFiles.get('/config.json')!); diff --git a/apps/web/src/api/hooks.ts b/apps/web/src/api/hooks.ts index e0654f9..12ca5d7 100644 --- a/apps/web/src/api/hooks.ts +++ b/apps/web/src/api/hooks.ts @@ -235,7 +235,8 @@ export function useSetPerformanceSettings(slug: string) { `/api/servers/${slug}/config/performance`, settings, ), - onSuccess: () => { + onSuccess: (result) => { + queryClient.setQueryData(['servers', slug, 'config', 'performance'], result); void queryClient.invalidateQueries({ queryKey: ['servers', slug] }); }, }); diff --git a/apps/web/src/components/performance-form.tsx b/apps/web/src/components/performance-form.tsx index 15b7dd7..f750c5c 100644 --- a/apps/web/src/components/performance-form.tsx +++ b/apps/web/src/components/performance-form.tsx @@ -52,6 +52,7 @@ const NUMBER_FIELDS: { key: NumberKey; label: string; min: number; max: number; ]; const BOOLEAN_FIELDS: { key: BooleanKey; label: string; hint: string }[] = [ + { key: 'disableAI', label: 'Disable AI', hint: 'default enabled' }, { key: 'disableThirdPerson', label: 'Disable third person', hint: 'default disabled' }, { key: 'fastValidation', label: 'Fast validation', hint: 'default enabled' }, { key: 'battlEye', label: 'BattlEye', hint: 'default enabled' }, diff --git a/apps/web/src/pages/simple-pages.tsx b/apps/web/src/pages/simple-pages.tsx index 518e881..4bbd61f 100644 --- a/apps/web/src/pages/simple-pages.tsx +++ b/apps/web/src/pages/simple-pages.tsx @@ -42,7 +42,7 @@ function ConfigurationsBody({ slug, user }: { slug: string; user: CurrentUser })