diff --git a/docs/performance-config-integration.md b/docs/performance-config-integration.md index d80c3e72..1943545e 100644 --- a/docs/performance-config-integration.md +++ b/docs/performance-config-integration.md @@ -5,7 +5,7 @@ strategy is "curate one upstream" rather than "ingest any config from GameBanana." Read this before touching `performanceConfig.ts`, `performanceConfigData.ts`, or building the planned manifest/preset UI. -Status: six selectable presets shipped, generated from pinned upstream commits. +Status: seven selectable presets shipped, generated from pinned upstream commits. The research that drove the scope decision is recorded below and still holds; "What shipped" records where the delivered design differs from the plan it replaced. @@ -98,7 +98,7 @@ intersection is extractable and safe; everything beyond it is author-specific. ## What shipped -Six presets selected by id: `sqooky-default` (balanced, default), `sqooky-testing` +Seven presets selected by id: `sqooky-default` (balanced, default), `eskay` (light), `sqooky-testing` (preview), `boot-max-fps` (aggressive), `kaizu-min-spec` (potato), `optilock-fps` (competitive), `optilock-max` (maximum). Each is a section/key diff of a pinned upstream `gameinfo.gi` against the stock baseline, generated into @@ -133,6 +133,15 @@ edit. The latter access-violates in the current Deadlock build when combined with boot's convar body; live launch bisection confirmed the full boot preset stays running without that one edit. No other bundled preset currently sets it. +`MaterialSystem2/RenderModes` is excluded as a whole section because it is a +list (seven `game` entries), not key/value pairs. The parser keeps the last +duplicate and the patcher edits every occurrence of a key, so configs written +before Valve added `ShadowSilhouette` (boot, OptiLock) diffed to +`game "FrontDepth"` and collapsed all seven render modes into one. Track-latest +users on v1.30.x hit this at runtime; `latestAsPreset` re-applies today's +section exclusions to cached bodies so the fix reaches them without a refetch. +Any future list-valued section needs the same treatment. + ### Marker grammar The block header is the authoritative record of what is in the file; the sidecar diff --git a/electron/main/services/performanceConfig.test.ts b/electron/main/services/performanceConfig.test.ts index f1d46e1c..e1ca3090 100644 --- a/electron/main/services/performanceConfig.test.ts +++ b/electron/main/services/performanceConfig.test.ts @@ -20,7 +20,8 @@ import { removePerformanceConfig, resetPerformanceConfigOverrides, } from './performanceConfig'; -import { getPreset } from './performanceConfigData'; +import { CLASSIFICATION, getPreset } from './performanceConfigData'; +import { pathExcluded } from './performancePresetGen'; const STOCK = readFileSync(join(__dirname, '__fixtures__/stock-gameinfo.gi'), 'utf-8'); const STOCK_CRLF = STOCK.split('\n').join('\r\n'); @@ -176,6 +177,14 @@ describe('preset version history', () => { } }); + // `perf:presets --check` is not in CI (it needs the network), so this is + // the offline guard against a regenerated body editing an excluded + // section, e.g. the list-valued RenderModes. + it.each(ALL)('%s v%s: no section op lands in an excluded section', (id, version) => { + const ops = getPreset(id, version).sectionOps; + expect(ops.filter((op) => pathExcluded(op.path, CLASSIFICATION.excludeSections))).toEqual([]); + }); + it.each(ALL)('%s v%s: apply then remove restores the file byte for byte', (id, version) => { expect(applyPerformanceConfig(gameRoot, { presetId: id, version }).state).toBe('applied'); expect(read()).not.toBe(STOCK); diff --git a/electron/main/services/performanceConfigData.ts b/electron/main/services/performanceConfigData.ts index 01c763e5..d3c304e6 100644 --- a/electron/main/services/performanceConfigData.ts +++ b/electron/main/services/performanceConfigData.ts @@ -43,6 +43,7 @@ export type OptInGroup = 'visibility' | 'camera' | 'devtools'; export type PresetTier = 'balanced' + | 'light' | 'preview' | 'aggressive' | 'potato' @@ -134,7 +135,7 @@ export interface PerformancePreset { * path also tell the track-latest path where a fresh baseline lives. */ export const BASELINE = { repo: 'Sqooky/OptimizationLock', - commit: '2b994f62a84ba90cd666c7c63674a7aab3127aca', + commit: 'e2e9925cdd9d564a1a0bb19f28185211fb0dea75', path: 'clean gameinfo.gi/gameinfo.gi', } as const; @@ -149,6 +150,7 @@ export const CLASSIFICATION: PresetGenClassification = { ['ResourceCompiler'], ['ContentBuilder'], ['SteamAudio'], + ['MaterialSystem2', 'RenderModes'], ], excludeKeys: [ 'AllowKeyChordBindings', @@ -245,10 +247,10 @@ const SqookyDefault: PerformancePresetFamily = { version: '3.4', ref: '3.4', refKind: 'prose', - commit: '2b994f62a84ba90cd666c7c63674a7aab3127aca', - historyCommit: '2b994f62a84ba90cd666c7c63674a7aab3127aca', - date: '2026-10-03', - sha256: '041ebe91c92548c7a45dc4832d22383cf5ce190d8d75b0bb734075f203c02113', + commit: 'e2e9925cdd9d564a1a0bb19f28185211fb0dea75', + historyCommit: 'bb69d9d0f49e7b609b2248b05175c515e86c588d', + date: '2026-10-05', + sha256: 'c1e451134cd1d4d57d0f665673f9bbe732d6335c2b48ee6617079c23254222c7', sectionOps: [ { path: ['Particles'], key: 'EnableMixedResolution', value: '1' }, { path: ['Particles'], key: 'MPropertyFlattenIntoParentRow', value: '1' }, @@ -1170,6 +1172,327 @@ const SqookyDefault: PerformancePresetFamily = { ], }; +const Eskay: PerformancePresetFamily = { + id: 'eskay', + name: 'Eskay\'s Config', + tier: 'light', + author: 'Eskay', + upstream: { + repo: 'Sqooky/OptimizationLock', + url: 'https://github.com/Sqooky/OptimizationLock', + path: 'Eskay\'s config/gameinfo.gi', + paths: ['Eskay\'s config/gameinfo.gi'], + license: 'GPL-3.0', + credit: 'OptimizationLock by Sqooky and contributors', + }, + releases: [ + { + version: '3.4', + ref: '3.4', + refKind: 'prose', + commit: 'e2e9925cdd9d564a1a0bb19f28185211fb0dea75', + historyCommit: 'bb69d9d0f49e7b609b2248b05175c515e86c588d', + date: '2026-10-05', + sha256: 'c7a496cd9c4b62de03ae03f4bbfda9cd8228bf97cdd1a0824aafe441809dd3ba', + sectionOps: [ + { path: ['Particles'], key: 'EnableMixedResolution', value: '1' }, + { path: ['Particles'], key: 'MPropertyFlattenIntoParentRow', value: '1' }, + { path: ['Particles'], key: 'ParticleTraceOffsetOnlyHit', value: '1' }, + { path: ['Particles'], key: 'ParticlesFoggedByDefault', value: '0' }, + { path: ['Particles'], key: 'PerVertexLighting', value: '0' }, + { path: ['Particles'], key: 'PostSimulate', value: '0' }, + { path: ['RenderSystem'], key: 'AllowPartialMipChainImmediateTexLoads', value: '1' }, + { path: ['RenderSystem'], key: 'GraphicsPipelineLibrary', value: '1' }, + { path: ['RenderSystem'], key: 'IndexBufferPoolSizeMB', value: '128' }, + { path: ['RenderSystem'], key: 'MaxPreloadTextureResolution', value: '0' }, + { path: ['RenderSystem'], key: 'MinStreamingPoolSizeMB', value: '2048' }, + { path: ['RenderSystem'], key: 'SwapChainSampleableDepth', value: '1' }, + { path: ['RenderSystem'], key: 'VulkanAdditionalShaderCache', value: 'vulkan_shader_cache.foz' }, + { path: ['RenderSystem'], key: 'VulkanOnly_Linux', value: '1' }, + { path: ['RenderSystem'], key: 'VulkanStagingPMBSizeLimitMB', value: '768' }, + { path: ['RenderSystem'], key: 'VulkanSteamAppShaderCache', value: '1' }, + { path: ['RenderSystem'], key: 'VulkanSteamDownloadedShaderCache', value: '1' }, + { path: ['RenderSystem'], key: 'VulkanSteamShaderCache', value: '1' }, + { path: ['SceneSystem'], key: 'CSMCascadeResolution', value: '0' }, + { path: ['SceneSystem'], key: 'CharacterDecals', value: '0' }, + { path: ['SceneSystem'], key: 'CubemapFog', value: '0' }, + { path: ['SceneSystem'], key: 'DefaultShadowTextureHeight', value: '0' }, + { path: ['SceneSystem'], key: 'DefaultShadowTextureWidth', value: '0' }, + { path: ['SceneSystem'], key: 'DisableLateAllocatedTransformBuffer', value: '1' }, + { path: ['SceneSystem'], key: 'DisableShadowFullSort', value: '1' }, + { path: ['SceneSystem'], key: 'EnableAlphaTint', value: '0' }, + { path: ['SceneSystem'], key: 'EnableSunlight', value: '0' }, + { path: ['SceneSystem'], key: 'EnableViewModelSunlight', value: '0' }, + { path: ['SceneSystem'], key: 'FogCachedShadowAtlasHeight', value: '0' }, + { path: ['SceneSystem'], key: 'FogCachedShadowAtlasWidth', value: '0' }, + { path: ['SceneSystem'], key: 'FogCachedShadowTileMaxFilterRadius', value: '0' }, + { path: ['SceneSystem'], key: 'FogCachedShadowTileSize', value: '0' }, + { path: ['SceneSystem'], key: 'GpuLightBinnerBinEnvMaps', value: '1' }, + { path: ['SceneSystem'], key: 'GpuLightBinnerBinLPVs', value: '0' }, + { path: ['SceneSystem'], key: 'GpuLightBinnerSupportViewModelCascade', value: '0' }, + { path: ['SceneSystem'], key: 'HDRFrameBuffer', value: '0' }, + { path: ['SceneSystem'], key: 'HairShading', value: 'false' }, + { path: ['SceneSystem'], key: 'LayerBatchThresholdFullsort', value: '200' }, + { path: ['SceneSystem'], key: 'LightCookieAllocGranularity', value: '1' }, + { path: ['SceneSystem'], key: 'LightCookieMinAllocSize', value: '0' }, + { path: ['SceneSystem'], key: 'MinimumLateAllocatedVertexCacheBufferSizeMB', value: '64' }, + { path: ['SceneSystem'], key: 'NonTexturedGradientFog', value: '0' }, + { path: ['SceneSystem'], key: 'ParticleBufferSize', value: '512' }, + { path: ['SceneSystem'], key: 'PerVertexLighting', value: '0' }, + { path: ['SceneSystem'], key: 'PointLightShadowsEnabled', value: '0' }, + { path: ['SceneSystem'], key: 'PunctualContactShadows', value: '0' }, + { path: ['SceneSystem'], key: 'SelfShadowStrength', value: '0' }, + { path: ['SceneSystem'], key: 'ShadowAtlas', value: '0' }, + { path: ['SceneSystem'], key: 'ShadowDepth', value: '0' }, + { path: ['SceneSystem'], key: 'ShadowDepthBuffer', value: '0' }, + { path: ['SceneSystem'], key: 'ShadowDepthBufferNoCmp', value: '0' }, + { path: ['SceneSystem'], key: 'ShadowmapMaxFilterRadius', value: '0' }, + { path: ['SceneSystem'], key: 'SparseShadowTrees', value: '0' }, + { path: ['SceneSystem'], key: 'SunLightMaxCascadeSize', value: '2' }, + { path: ['SceneSystem'], key: 'SupportsInstancedFade', value: '0' }, + { path: ['SceneSystem'], key: 'VolumetricFog', value: '0' }, + { path: ['WorldRenderer'], key: 'AggregateInstanceStream', value: '1' }, + { path: ['WorldRenderer'], key: 'AggregateRTProxyDesc', value: '1' }, + { path: ['WorldRenderer'], key: 'AggregateSceneObjectDesc', value: '1' }, + { path: ['WorldRenderer'], key: 'AggregateVertexColorStream', value: '1' }, + { path: ['WorldRenderer'], key: 'EnvironmentMapCacheSize', value: '1024' }, + { path: ['WorldRenderer'], key: 'EnvironmentMapCacheSizeTools', value: '2' }, + { path: ['WorldRenderer'], key: 'GrassCastsShadows', value: '0' }, + { path: ['WorldRenderer'], key: 'LPVEdgeBlending', value: '0' }, + ], + convars: [ + ['citadel_portrait_world_renderer_off', 'false'], + ['cl_phys_enabled', 'true'], + ['lb_enable_dynamic_lights', 'true'], + ['r_citadel_enable_pano_world_blur', 'true'], + ['r_particle_explicit_fetch', 'false'], + ['r_particle_max_size_cull', '900'], + ['r_size_cull_threshold', '0.9'], + ['sc_screen_size_lod_scale_override', '0.55'], + ['steam_inputhandler_enabled', 'true'], + ['citadel_damage_offscreen_indicator_disabled', 'true'], + ['citadel_damage_report_enable', '1'], + ['citadel_damage_text_batching_window_ability', '1000'], + ['citadel_distance_mouse_move_for_minimap_drawing', '1'], + ['citadel_hud_objective_health_debug_show_midboss', 'false'], + ['citadel_hud_objective_health_enabled', '2'], + ['citadel_unit_status_old_update_rate', '15'], + ['v8_maximum_heap_size_mb', '1024'], + ['panorama_comp_layer_lru_lifetime', '4'], + ['lb_enable_baked_shadows', 'false'], + ['lb_enable_stationary_lights', 'false'], + ['engine_low_latency_sleep_after_client_tick', 'false'], + ['panorama_max_fps', '30'], + ['panorama_max_overlay_fps', '30'], + ['citadel_melee_shake_duration', '0'], + ['citadel_camera_soft_collision_angle', '75'], + ['citadel_camera_use_vmdl_flatten_horizontal', 'false'], + ['citadel_camera_wobble_disable', 'true'], + ['citadel_melee_shake_amplitude', '0'], + ['engine_accurate_input_processing_delta_time', 'true'], + ['r_citadel_clip_sphere_min_opacity', '0'], + ['cam_idealdelta', '0'], + ['cam_ideallag', '0'], + ['citadel_camera_height', '0'], + ['citadel_camera_height_ceiling_distance', '0'], + ['citadel_camera_listening_offset', '-1'], + ['citadel_camera_see_distance_max', '7000'], + ['citadel_shoot_forward_offset', '0'], + ['citadel_stuck_camera_trace_extra_length', '0'], + ['citadel_tightcamera_alternative', '1'], + ['nav_edit_use_camera', '0'], + ['rpg_camera_yaw', '0'], + ['r_texture_budget_threshold', '0.7'], + ['r_texture_budget_update_period', '0.5'], + ['r_texture_stream_mip_bias', '3'], + ['r_texturefilteringquality', '0'], + ['thread_pool_option', '2'], + ['closecaption', 'false'], + ['panorama_allow_transitions', 'false'], + ['panorama_disable_blur', 'true'], + ['panorama_panel_occlusion', 'true'], + ['r_dashboard_render_quality', '1'], + ['lb_barnlight_shadowmap_scale', '0'], + ['lb_csm_cascade_size_override', '1'], + ['lb_csm_draw_alpha_tested', '0'], + ['lb_csm_draw_translucent', '0'], + ['lb_csm_override_staticgeo_cascades', 'true'], + ['lb_csm_override_staticgeo_cascades_value', 'true'], + ['lb_dynamic_shadow_resolution_base', '16'], + ['lb_enable_shadow_casting', '0'], + ['lb_ssss_samples', '0'], + ['lb_sun_csm_size_cull_threshold_texels', '60'], + ['r_citadel_shadow_caching', 'true'], + ['r_citadel_shadow_quality', '0'], + ['r_size_cull_threshold_shadow', '2.4'], + ['sparseshadowtree_disable_for_viewmodel', '1'], + ['sparseshadowtree_enable_rendering', '0'], + ['cl_retire_low_priority_lights', '1'], + ['mat_async_shader_load', '1'], + ['mat_max_lighting_complexity', '0'], + ['r_citadel_ssao_quality', '0'], + ['r_citadel_ssao_thin_occluder_compensation', '0'], + ['r_citadel_sun_shadow_slope_scale_depth_bias', '0'], + ['r_distancefield_enable', '1'], + ['r_lightmap_bicubic_filtering', '1'], + ['r_lightmap_size', '2048'], + ['r_lightmap_size_directional_irradiance', '0'], + ['r_multiscattering', '1'], + ['r_rendersun', '0'], + ['r_ssao', '0'], + ['r_ssao_strength', '0'], + ['cl_disable_ragdolls', '0'], + ['cl_ragdoll_limit', '0'], + ['cl_fasttempentcollision', '1000'], + ['cloth_sim_on_tick', '0'], + ['enable_boneflex', 'false'], + ['ik_fabrik_align_chain', '1'], + ['ik_final_fixup_enable', 'false'], + ['props_break_max_pieces_perframe', '1'], + ['cl_show_splashes', '0'], + ['mat_colorcorrection', '1'], + ['r_character_decal_resolution', '4'], + ['r_depth_of_field', '0'], + ['r_effects_bloom', '0'], + ['r_post_bloom', '0'], + ['sc_clutter_enable', 'false'], + ['violence_ablood', '0'], + ['violence_agibs', '0'], + ['violence_hblood', '0'], + ['violence_hgibs', '0'], + ['volume_fog_intermediate_textures_hdr', 'false'], + ['cl_particle_sim_fallback_base_multiplier', '100'], + ['cl_particle_sim_fallback_threshold_ms', '1'], + ['cl_particle_fallback_multiplier', '10'], + ['cl_particle_fallback_base', '5'], + ['cl_particle_batch_mode', '1'], + ['r_citadel_screenspace_particles_full_res', 'true'], + ['r_draw_particle_children_with_parents', '1'], + ['r_limit_particle_job_duration', 'true'], + ['r_particle_allowprerender', 'true'], + ['r_particle_fixedrandomseeds', 'true'], + ['r_particle_max_texture_layers', '4'], + ['r_particle_min_timestep', '0.00241'], + ['r_particle_model_per_thread_count', '64'], + ['r_particle_skip_postsim', 'true'], + ['r_physics_particle_op_spawn_scale', '0'], + ['r_update_particles_on_render_only_frames', 'true'], + ['r_world_wind_strength', '0'], + ['phys_cull_internal_mesh_contacts', 'true'], + ['sc_aggregate_bvh_threshold', '256'], + ['sc_allow_dithered_lod', 'false'], + ['sc_instanced_mesh_motion_vectors', '0'], + ['sc_instanced_mesh_size_cull_bias_shadow', '10'], + ['sc_layer_batch_threshold', '256'], + ['citadel_video_preset', '9'], + ['r_citadel_gpu_culling', 'true'], + ['r_vma_defrag_algorithm', '0'], + ['rtx_dynamic_blas', 'false'], + ['rtx_dynamic_blas_caching', 'true'], + ['rtx_force_default_hitgroup', 'true'], + ['rtx_texture_resolution', '64'], + ['sc_instanced_mesh_opaque_fade', 'false'], + ['snd_steamaudio_max_occlusion_samples', '32'], + ['snd_steamaudio_num_diffuse_samples', '512'], + ['cc_captiontrace', '0'], + ['citadel_bullet_shot_offset_fade_time', '0'], + ['citadel_show_survey', 'true'], + ['citadel_test_ranked_summary', 'true'], + ['cl_batch_entity_list_ops_during_latch', 'true'], + ['cl_enable_eye_occlusion', 'false'], + ['cl_modifier_parallel_gather_status_effect_updates', 'false'], + ['cl_phys_assume_fixed_tick_interval', 'true'], + ['csm_viewmodel_farz', '1'], + ['csm_viewmodel_max_shadow_dist', '1'], + ['csm_viewmodel_max_visible_dist', '1'], + ['csm_viewmodel_nearz', '512'], + ['debug_draw_enable', 'false'], + ['engine_max_ticks_to_simulate', '2'], + ['r_async_compute_fog', 'true'], + ['r_citadel_depth_prepass_dynamic_objects', 'false'], + ['r_citadel_glow_health_bar_debug', 'false'], + ['r_citadel_gpu_preview_denoise_passes', '0'], + ['r_drawropes', 'false'], + ['r_drawtracers_firstperson', 'false'], + ['r_drawviewmodel', 'false'], + ['r_enable_rigid_animation', 'false'], + ['r_hair_ao', '0'], + ['r_max_portal_render_targets', '2'], + ['r_particle_model_new', 'false'], + ['r_particle_model_new8', 'false'], + ['r_particle_newinput', 'true'], + ['r_pixelvisibility_partial', 'false'], + ['r_render_hair', 'false'], + ['r_renderdoc_auto_shader_pdbs', 'false'], + ['r_skip_precache_validation_check', 'true'], + ['r_strip_invisible_during_sceneobject_update', '1'], + ['sparseshadowtree_leaf_precision_viewmodel', '0'], + ['r_grass_end_fade', '0'], + ['r_grass_quality', '0'], + ['r_grass_start_fade', '0'], + ['cl_simulate_dormant_entities', 'false'], + ['audio_enable_vmix_mastering', 'false'], + ['snd_mixahead', '0.05'], + ['snd_occlusion_bounces', '0'], + ['snd_occlusion_rays', '0'], + ['snd_soundmixer_version', '2'], + ['snd_steamaudio_reverb_order_rendering', '0'], + ['snd_steamaudio_num_threads', '6'], + ['audio_enable_spawn_mask_mix_layer', 'false'], + ['snd_boxverb_simd', 'false'], + ['snd_enable_subgraph_corenull_passthrough', 'false'], + ['csm_cascade0_override_dist', '0'], + ['csm_cascade1_override_dist', '0'], + ['csm_cascade2_override_dist', '0'], + ['csm_cascade3_override_dist', '0'], + ['csm_max_dist_between_caster_and_receiver', '0'], + ['csm_max_num_cascades_override', '0'], + ['csm_max_visible_dist', '0'], + ['csm_res_override_0', '1'], + ['csm_res_override_1', '1'], + ['csm_res_override_2', '1'], + ['csm_res_override_3', '1'], + ['csm_viewmodel_shadows', 'false'], + ['ik_constraints_enabled', 'false'], + ['ik_debug_dogleg3bone_enabled', 'false'], + ['ik_debug_fabrik_backwards_enabled', 'false'], + ['ik_debug_fabrik_forwards_enabled', 'false'], + ['ik_enable', 'false'], + ['ik_fabrik_backwards_enabled', 'false'], + ['ik_fabrik_forwards_enabled', 'false'], + ['ik_planetilt_enable', 'false'], + ['animgraph_footlock_calculate_tilt', 'false'], + ['animgraph_footlock_enabled', 'false'], + ['animgraph_footlock_ground_roll', 'false'], + ['animgraph_footlock_hip_offset_enable', 'false'], + ['animgraph_footlock_ik_enable', 'false'], + ['animgraph_footlock_trace_ground_enabled', 'false'], + ['animgraph_footlock_use_hip_shift', 'false'], + ['animgraph_slowdownonslopes_enabled', 'false'], + ['fog_enable', 'false'], + ['fog_enableskybox', 'false'], + ['volume_fog_enable_jitter', 'false'], + ['citadel_npc_force_animate_every_tick', 'false'], + ], + optIn: [ + { key: 'citadel_trooper_glow_disabled', value: '1', group: 'visibility' }, + { key: 'citadel_boss_glow_disabled', value: '1', group: 'visibility' }, + { key: 'citadel_unit_status_allies_see_thru_walls', value: 'true', group: 'visibility' }, + { key: 'citadel_unit_status_allies_see_thru_walls_max_distance', value: '40', group: 'visibility' }, + { key: 'citadel_unit_status_dpi', value: '10', group: 'visibility' }, + { key: 'r_aspectratio', value: '2.2', group: 'camera' }, + { key: 'citadel_unit_status_single_bar_mode', value: 'false', group: 'visibility' }, + { key: 'citadel_unit_status_use_new', value: 'true', group: 'visibility' }, + { key: 'citadel_unit_status_use_v2', value: '0', group: 'visibility' }, + { key: 'citadel_unit_status_use_v2_for_nonplayers', value: '0', group: 'visibility' }, + { key: 'citadel_camera_pitch_default', value: '0', group: 'camera' }, + { key: 'default_fov', value: '0', group: 'camera' }, + { key: 'viewmodel_fov', value: '0', group: 'camera' }, + ], + }, + ], +}; + const SqookyTesting: PerformancePresetFamily = { id: 'sqooky-testing', name: 'Sqooky\'s Testing', @@ -1189,10 +1512,10 @@ const SqookyTesting: PerformancePresetFamily = { version: '3.4', ref: '3.4', refKind: 'prose', - commit: '2b994f62a84ba90cd666c7c63674a7aab3127aca', - historyCommit: '2b994f62a84ba90cd666c7c63674a7aab3127aca', - date: '2026-10-03', - sha256: '9f77fb7a2ece33cb2a085a0cc2c23be6dd9e5bb61fa212e5ebe90dcb998e0037', + commit: 'e2e9925cdd9d564a1a0bb19f28185211fb0dea75', + historyCommit: 'bb69d9d0f49e7b609b2248b05175c515e86c588d', + date: '2026-10-05', + sha256: '96ed6e4ceb5ec482cadb38756012cd317203694fe3aa6dcbb082c39d41f98ecc', sectionOps: [ { path: ['Particles'], key: 'EnableMixedResolution', value: '1' }, { path: ['Particles'], key: 'MPropertyFlattenIntoParentRow', value: '1' }, @@ -2219,16 +2542,15 @@ const BootMaxFps: PerformancePresetFamily = { version: '3.4', ref: '3.4', refKind: 'prose', - commit: '2b994f62a84ba90cd666c7c63674a7aab3127aca', + commit: 'e2e9925cdd9d564a1a0bb19f28185211fb0dea75', historyCommit: '4f900690efc233ab10e88d4747881f87e2c5d9f0', - date: '2026-10-03', + date: '2026-10-05', sha256: 'f09e6cea24a0f01b549d3b0b87277de51a7f0f1802305cfb9e6da63bce2d2a09', // Byte-identical upstream in 3.3, 3.2, collapsed into this entry. sectionOps: [ { path: ['Engine2'], key: 'LightmapUVQuery', value: '0' }, { path: ['Engine2', 'RenderingPipeline'], key: 'AmbientOcclusionProxies', value: '0' }, { path: ['Localize'], key: 'DisallowTokenContexts', value: '1' }, - { path: ['MaterialSystem2', 'RenderModes'], key: 'game', value: 'FrontDepth' }, { path: ['ModelDoc'], key: 'features', value: 'animgraph;modelconfig;gamepreview;wireframe_backfaces;distancefield' }, { path: ['NVNGX'], key: 'ReflexLateWarp', value: '1' }, { path: ['NetworkSystem'], key: 'UseSerializedEntityPool', value: '1' }, @@ -2680,10 +3002,10 @@ const KaizuMinSpec: PerformancePresetFamily = { version: '3.4', ref: '3.4', refKind: 'prose', - commit: '2b994f62a84ba90cd666c7c63674a7aab3127aca', - historyCommit: '1da9517e8f0d0b636979d295b4bebcb7858ac6c9', - date: '2026-10-03', - sha256: 'afda3762de652cf04f325015cfe16496be4a0080e587de83e46baf753c3a36ad', + commit: 'e2e9925cdd9d564a1a0bb19f28185211fb0dea75', + historyCommit: 'e2e9925cdd9d564a1a0bb19f28185211fb0dea75', + date: '2026-10-05', + sha256: '46abb89c15bebdabef6234959cbe597df289176d3e2a19cf826381107e103bc6', sectionOps: [ { path: ['SceneSystem'], key: 'CSMCascadeResolution', value: '0' }, { path: ['SceneSystem'], key: 'CubemapFog', value: '0' }, @@ -3493,7 +3815,6 @@ const OptilockFps: PerformancePresetFamily = { { path: ['Engine2', 'RenderingPipeline'], key: 'AmbientOcclusionProxies', value: '0' }, { path: ['GameInfo'], key: 'DisallowGameInfoConditionals', value: '0' }, { path: ['Localize'], key: 'DisallowTokenContexts', value: '1' }, - { path: ['MaterialSystem2', 'RenderModes'], key: 'game', value: 'FrontDepth' }, { path: ['ModelDoc'], key: 'features', value: 'animgraph;modelconfig;gamepreview;wireframe_backfaces;distancefield' }, { path: ['NetworkSystem'], key: 'UseSerializedEntityPool', value: '1' }, { path: ['NetworkSystem', 'BetaUniverse'], key: 'FakeLag', value: '0' }, @@ -4056,7 +4377,6 @@ const OptilockFps: PerformancePresetFamily = { { path: ['Engine2', 'RenderingPipeline'], key: 'AmbientOcclusionProxies', value: '0' }, { path: ['GameInfo'], key: 'DisallowGameInfoConditionals', value: '0' }, { path: ['Localize'], key: 'DisallowTokenContexts', value: '1' }, - { path: ['MaterialSystem2', 'RenderModes'], key: 'game', value: 'FrontDepth' }, { path: ['ModelDoc'], key: 'features', value: 'animgraph;modelconfig;gamepreview;wireframe_backfaces;distancefield' }, { path: ['NetworkSystem'], key: 'UseSerializedEntityPool', value: '1' }, { path: ['NetworkSystem', 'BetaUniverse'], key: 'FakeLag', value: '0' }, @@ -4508,7 +4828,6 @@ const OptilockMax: PerformancePresetFamily = { { path: ['Engine2', 'RenderingPipeline'], key: 'AmbientOcclusionProxies', value: '0' }, { path: ['GameInfo'], key: 'DisallowGameInfoConditionals', value: '0' }, { path: ['Localize'], key: 'DisallowTokenContexts', value: '1' }, - { path: ['MaterialSystem2', 'RenderModes'], key: 'game', value: 'FrontDepth' }, { path: ['ModelDoc'], key: 'features', value: 'animgraph;modelconfig;gamepreview;wireframe_backfaces;distancefield' }, { path: ['NetworkSystem'], key: 'UseSerializedEntityPool', value: '1' }, { path: ['NetworkSystem', 'BetaUniverse'], key: 'FakeLag', value: '0' }, @@ -5051,7 +5370,6 @@ const OptilockMax: PerformancePresetFamily = { { path: ['Engine2', 'RenderingPipeline'], key: 'AmbientOcclusionProxies', value: '0' }, { path: ['GameInfo'], key: 'DisallowGameInfoConditionals', value: '0' }, { path: ['Localize'], key: 'DisallowTokenContexts', value: '1' }, - { path: ['MaterialSystem2', 'RenderModes'], key: 'game', value: 'FrontDepth' }, { path: ['ModelDoc'], key: 'features', value: 'animgraph;modelconfig;gamepreview;wireframe_backfaces;distancefield' }, { path: ['NetworkSystem'], key: 'UseSerializedEntityPool', value: '1' }, { path: ['NetworkSystem', 'BetaUniverse'], key: 'FakeLag', value: '0' }, @@ -5594,7 +5912,6 @@ const OptilockMax: PerformancePresetFamily = { { path: ['Engine2', 'RenderingPipeline'], key: 'AmbientOcclusionProxies', value: '0' }, { path: ['GameInfo'], key: 'DisallowGameInfoConditionals', value: '0' }, { path: ['Localize'], key: 'DisallowTokenContexts', value: '1' }, - { path: ['MaterialSystem2', 'RenderModes'], key: 'game', value: 'FrontDepth' }, { path: ['ModelDoc'], key: 'features', value: 'animgraph;modelconfig;gamepreview;wireframe_backfaces;distancefield' }, { path: ['NetworkSystem'], key: 'UseSerializedEntityPool', value: '1' }, { path: ['NetworkSystem', 'BetaUniverse'], key: 'FakeLag', value: '0' }, @@ -6129,6 +6446,7 @@ const OptilockMax: PerformancePresetFamily = { export const PRESETS: readonly PerformancePresetFamily[] = [ SqookyDefault, + Eskay, SqookyTesting, BootMaxFps, KaizuMinSpec, diff --git a/electron/main/services/performanceLatestCore.test.ts b/electron/main/services/performanceLatestCore.test.ts index 780e9bf4..a839950d 100644 --- a/electron/main/services/performanceLatestCore.test.ts +++ b/electron/main/services/performanceLatestCore.test.ts @@ -164,6 +164,28 @@ describe('buildLatestRelease gates', () => { } }); + it('leaves the list-valued RenderModes section alone', () => { + // Stock lists seven `game` render modes; a config written before Valve + // added ShadowSilhouette ends on FrontDepth, which used to diff to a + // `game "FrontDepth"` op that the patcher wrote over every mode. + const withModes = (text: string, modes: string[]) => + text.replace( + /\n\}\n$/, + `\n\tMaterialSystem2\n\t{\n\t\tRenderModes\n\t\t{\n${modes + .map((m) => `\t\t\tgame "${m}"`) + .join('\n')}\n\t\t}\n\t}\n}\n` + ); + const result = buildLatestRelease( + input({ + baselineText: withModes(baselineText(), ['Default', 'FrontDepth', 'ShadowSilhouette']), + configText: withModes(configText(), ['Default', 'FrontDepth']), + }) + ); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.release.sectionOps.some((op) => op.path[0] === 'MaterialSystem2')).toBe(false); + }); + it('refuses implausible texts', () => { expect(buildLatestRelease(input({ configText: 'tiny' })).ok).toBe(false); expect( @@ -269,6 +291,18 @@ describe('cache round-trip and resolution', () => { expect(preset.convars).toEqual(r.convars); expect(preset.optIn[0].group).toBe('visibility'); }); + + it('drops ops an older build cached for a section that is now excluded', () => { + const r = release(); + const renderModes = { path: ['MaterialSystem2', 'RenderModes'], key: 'game', value: 'FrontDepth' }; + writeLatestCache(dir, { + byPreset: { [r.presetId]: { ...r, sectionOps: [...r.sectionOps, renderModes] } }, + checkedAt: {}, + history: {}, + }); + const preset = resolveCachedPreset(dir, r.presetId, 'latest'); + expect(preset?.sectionOps).toEqual(r.sectionOps); + }); }); // --------------------------------------------------------------------------- diff --git a/electron/main/services/performanceLatestCore.ts b/electron/main/services/performanceLatestCore.ts index 93248862..804d06de 100644 --- a/electron/main/services/performanceLatestCore.ts +++ b/electron/main/services/performanceLatestCore.ts @@ -29,6 +29,7 @@ import { import { generatePresetBody, parseConfig, + pathExcluded, validateGameinfoText, validateGeneratedBody, } from './performancePresetGen'; @@ -249,7 +250,12 @@ export function latestAsPreset(release: LatestRelease): PerformancePreset { refKind: release.refKind, commit: release.commit, }, - sectionOps: release.sectionOps, + // A cached body was classified by whichever Grimoire fetched it. + // Today's section exclusions still apply: RenderModes was excluded + // after older builds had cached bodies that wrote to it. + sectionOps: release.sectionOps.filter( + (op) => !pathExcluded(op.path, CLASSIFICATION.excludeSections) + ), convars: release.convars, // Groups come from the same manifest tables the bundled data was // generated with, so the cast is between two views of one source. diff --git a/electron/main/services/performancePresetGen.ts b/electron/main/services/performancePresetGen.ts index 61eed9d1..845f250f 100644 --- a/electron/main/services/performancePresetGen.ts +++ b/electron/main/services/performancePresetGen.ts @@ -162,7 +162,7 @@ export function parseConfig(text: string): ParsedConfig { // Diff + classification // --------------------------------------------------------------------------- -function pathExcluded(path: string[], excludedSections: ReadonlyArray): boolean { +export function pathExcluded(path: string[], excludedSections: ReadonlyArray): boolean { return excludedSections.some((ex) => ex.every((seg, i) => path[i] === seg)); } diff --git a/scripts/gen-performance-presets.mjs b/scripts/gen-performance-presets.mjs index dbbe2b94..5c614959 100644 --- a/scripts/gen-performance-presets.mjs +++ b/scripts/gen-performance-presets.mjs @@ -51,12 +51,15 @@ function fail(msg) { // Fetch // --------------------------------------------------------------------------- -async function fetchAt(repo, commit, path) { +// `optional` turns a 404 into null instead of a failure. Only --refresh uses +// it, to record a preset whose file did not exist yet at an older release. +async function fetchAt(repo, commit, path, { optional = false } = {}) { const url = `https://raw.githubusercontent.com/${repo}/${commit}/${path .split('/') .map(encodeURIComponent) .join('/')}`; const res = await fetch(url); + if (optional && res.status === 404) return null; if (!res.ok) fail(`Fetch failed (${res.status}) for ${repo}@${commit.slice(0, 8)} ${path}`); return res.text(); } @@ -514,15 +517,38 @@ async function main() { // release changes relative to today's stock file", which is the only // thing that can be patched into the file the user actually has. const releases = []; + let absentAt = null; for (const release of source.releases) { const expected = entry.sha256[release.ref]; - if (!expected) { + if (expected === undefined) { fail( `Preset ${entry.id} has no sha256 for release ${release.ref}.\n` + ` Every release listed on source "${entry.source}" needs one, or the\n` + ` content gate silently stops covering that version.` ); } + // An explicit null records that the file did not exist upstream + // yet at this release (a config added after the source's older + // bundled releases). Only a trailing run of older releases may be + // absent: a file missing at the newest release was moved or + // deleted, which needs a pathByRef, not a silent skip. + if (expected === null) { + if (!releases.length) { + fail( + `Preset ${entry.id} has no file at ${release.ref}, the newest release of "${entry.source}".\n` + + ` Upstream moved or deleted ${pathFor(entry, release.ref)}. Point "path" at its new\n` + + ` location (with a pathByRef for older releases) or drop the preset.` + ); + } + absentAt ??= release.ref; + continue; + } + if (absentAt) { + fail( + `Preset ${entry.id} is absent at ${absentAt} but present at the older ${release.ref}.\n` + + ` Only releases older than the preset's first appearance may be null.` + ); + } const path = pathFor(entry, release.ref); const text = await fetchAt(source.repo, release.commit, path); verify(text, expected, `${entry.id} (${path}) at ${release.ref}`); @@ -613,7 +639,7 @@ async function main() { } } - // The opt-in list is only as good as the last hand-audit of six upstream + // The opt-in list is only as good as the last hand-audit of the upstream // files. The pattern check (now inside generatePresetBody) is what makes it // hold across a `--refresh`: a key that looks like a visibility or framing // setting has to be classified on purpose before it can ship in a preset @@ -736,9 +762,18 @@ async function refreshPins(manifest, target) { const source = manifest.sources[entry.source]; const hashes = {}; for (const release of source.releases) { - hashes[release.ref] = sha256( - await fetchAt(source.repo, release.commit, pathFor(entry, release.ref)) - ); + const text = await fetchAt(source.repo, release.commit, pathFor(entry, release.ref), { + optional: true, + }); + hashes[release.ref] = text === null ? null : sha256(text); + // A 404 is either "not written yet" or "lived at another path", + // and only the first is a correct null. + if (text === null) { + console.log( + ` ${entry.id}: no ${pathFor(entry, release.ref)} at ${release.ref}, recorded null.\n` + + ` ^ if the file lived elsewhere then, add a pathByRef instead.` + ); + } } entry.sha256 = hashes; } diff --git a/scripts/performance-presets.json b/scripts/performance-presets.json index d59a7831..ea2250ed 100644 --- a/scripts/performance-presets.json +++ b/scripts/performance-presets.json @@ -25,14 +25,17 @@ "", "To update a pin deliberately: pnpm perf:presets --refresh ", "then pnpm perf:presets to regenerate, and review the diff.", - "Never hand-edit electron/main/services/performanceConfigData.ts." + "Never hand-edit electron/main/services/performanceConfigData.ts.", + "", + "A preset added upstream after a source's older releases records null for", + "those releases: the file did not exist yet, so there is nothing to bundle." ], "historyDepth": 3, "$historyDepthComment": "How many upstream releases to bundle per source, newest first. --refresh prepends the new release and ages out the oldest. Raising it grows the main-process bundle by roughly 25 KB per preset per release; lowering it shortens how far a user can roll back.", "baseline": { "$comment": "Stock gameinfo.gi. Every preset is stored as a diff against this, which is what lets Grimoire patch in place instead of replacing the user's file.", "repo": "Sqooky/OptimizationLock", - "commit": "2b994f62a84ba90cd666c7c63674a7aab3127aca", + "commit": "e2e9925cdd9d564a1a0bb19f28185211fb0dea75", "path": "clean gameinfo.gi/gameinfo.gi", "sha256": "b96954f5607e381b94b3d5ba2214bf1bcee2b65a16bb1a231b5f840586466955" }, @@ -46,8 +49,8 @@ "releases": [ { "ref": "3.4", - "commit": "2b994f62a84ba90cd666c7c63674a7aab3127aca", - "date": "2026-10-03" + "commit": "e2e9925cdd9d564a1a0bb19f28185211fb0dea75", + "date": "2026-10-05" }, { "ref": "3.3", @@ -60,7 +63,7 @@ "date": "2026-09-30" } ], - "$comment": "Upstream publishes no git tags or releases; each 'ref' is the version stated in that release's commit message and in the config's own header comment. Pins are the commit SHAs. Sqooky follows a release with fix-up pushes (3.3 was fixed by \"IWRONG VALUE\" minutes later), so each pin is the last commit of that release's push burst, not the commit titled with the version. The newest release is also the baseline pin. If Sqooky starts tagging, switch refKind to 'tag' and pin the tags' resolved SHAs." + "$comment": "Upstream publishes no git tags or releases; each 'ref' is the version stated in that release's commit message and in the config's own header comment. Pins are the commit SHAs. Sqooky follows a release with fix-up pushes (3.3 was fixed by \"IWRONG VALUE\" minutes later), so each pin is the last commit of that release's push burst, not the commit titled with the version. The newest release is also the baseline pin. If Sqooky starts tagging, switch refKind to 'tag' and pin the tags' resolved SHAs. 3.4 is pinned past its push burst to e2e9925c (2026-10-05): those later commits only reformatted the bundled files (no key changes) and added Eskay's config, and upstream still states 3.4." }, "optilock": { "repo": "dacooderr/OptiLock", @@ -97,12 +100,26 @@ "source": "optimizationlock", "path": "Sqooky's .gi/gameinfo.gi", "sha256": { - "3.4": "041ebe91c92548c7a45dc4832d22383cf5ce190d8d75b0bb734075f203c02113", + "3.4": "c1e451134cd1d4d57d0f665673f9bbe732d6335c2b48ee6617079c23254222c7", "3.3": "dbb91f6e0ba60e9e9493fa823108036f13f2cf90997766f589296520319fb925", "3.2": "6bcc472f5f4132f9c39efbf88e747f266805952b3d00d8ab96bec5ae87d181f5" }, "author": "Sqooky" }, + { + "id": "eskay", + "name": "Eskay's Config", + "tier": "light", + "source": "optimizationlock", + "path": "Eskay's config/gameinfo.gi", + "$comment": "Added upstream after 3.3, so older releases are null. A minor fork of Sqooky's default that drops the far-clip and fade-distance culling (r_farz, r_mapextents, sc_fade_distance_scale_override), so no building or player pop-in.", + "sha256": { + "3.4": "c7a496cd9c4b62de03ae03f4bbfda9cd8228bf97cdd1a0824aafe441809dd3ba", + "3.3": null, + "3.2": null + }, + "author": "Eskay" + }, { "id": "sqooky-testing", "name": "Sqooky's Testing", @@ -110,7 +127,7 @@ "source": "optimizationlock", "path": "test_cfg/gameinfo.gi", "sha256": { - "3.4": "9f77fb7a2ece33cb2a085a0cc2c23be6dd9e5bb61fa212e5ebe90dcb998e0037", + "3.4": "96ed6e4ceb5ec482cadb38756012cd317203694fe3aa6dcbb082c39d41f98ecc", "3.3": "b7b2d540fdb74d91ca8544cb50354ca6ef1cc78dd9a24bc59ad0f5180e29abe1", "3.2": "93a375143673eae81e98adb0ddf7f24aeeb428a9bd7c696717d2db6ddaf46808" }, @@ -137,7 +154,7 @@ "source": "optimizationlock", "path": "kaizuchanerus minimum spec/gameinfo.gi", "sha256": { - "3.4": "afda3762de652cf04f325015cfe16496be4a0080e587de83e46baf753c3a36ad", + "3.4": "46abb89c15bebdabef6234959cbe597df289176d3e2a19cf826381107e103bc6", "3.3": "e85b31c337752597faaf25a4e628d687f67684fdf5284607141b13e2554c7b74", "3.2": "e1a58a6dc07df47a0e08bdc031fefec90da89070094e5c4a248872bc020cc80d" }, @@ -184,7 +201,7 @@ } ], "exclude": { - "$comment": "Never emitted into a preset, whatever upstream sets.", + "$comment": "Never emitted into a preset, whatever upstream sets. MaterialSystem2/RenderModes is a list of seven `game` entries, not key/value pairs: a config written before Valve added ShadowSilhouette diffed to a single `game \"FrontDepth\"` op, and the patcher, which edits every occurrence of a key, collapsed all seven render modes into FrontDepth.", "patterns": [ { "pattern": "^(host_)?timescale$", @@ -213,6 +230,10 @@ ], [ "SteamAudio" + ], + [ + "MaterialSystem2", + "RenderModes" ] ], "keys": [ @@ -297,7 +318,7 @@ }, { "key": "citadel_unit_status_old_update_rate", - "why": "Caught by the citadel_unit_status_ pattern, but it is the one member of that family that is genuinely a frame-rate setting: it caps how often health bars redraw. All six presets set 15 against a stock default of 30. It changes redraw cost, not what the bars show." + "why": "Caught by the citadel_unit_status_ pattern, but it is the one member of that family that is genuinely a frame-rate setting: it caps how often health bars redraw. Every bundled preset except kaizuchanerus' sets 15 against a stock default of 30. It changes redraw cost, not what the bars show." }, { "key": "citadel_hud_objective_health_debug_show_midboss", diff --git a/src/assets/performance-authors/eskay.png b/src/assets/performance-authors/eskay.png new file mode 100644 index 00000000..444852c1 Binary files /dev/null and b/src/assets/performance-authors/eskay.png differ diff --git a/src/assets/performance-authors/kaizuchanerus.jpg b/src/assets/performance-authors/kaizuchanerus.jpg new file mode 100644 index 00000000..043f598b Binary files /dev/null and b/src/assets/performance-authors/kaizuchanerus.jpg differ diff --git a/src/components/performance/PerformanceConfigCard.test.tsx b/src/components/performance/PerformanceConfigCard.test.tsx index 3f95a819..ead97522 100644 --- a/src/components/performance/PerformanceConfigCard.test.tsx +++ b/src/components/performance/PerformanceConfigCard.test.tsx @@ -234,12 +234,11 @@ describe('PerformanceConfigCard', () => { it('shows bundled pictures for verified authors and an initial for the rest', async () => { await render(); - for (const tier of ['balanced', 'competitive', 'maximum', 'preview']) { + for (const tier of ['balanced', 'competitive', 'maximum', 'potato', 'preview']) { expect(card(tier)?.querySelector('img')).toBeTruthy(); } expect(card('aggressive')?.querySelector('img')).toBeNull(); expect(card('aggressive')?.textContent).toContain('b'); - expect(card('potato')?.querySelector('img')).toBeNull(); }); it('never shows commit hashes or raw status prose on the main card', async () => { diff --git a/src/components/performance/PerformanceConfigCard.tsx b/src/components/performance/PerformanceConfigCard.tsx index 49704cd2..610ae95a 100644 --- a/src/components/performance/PerformanceConfigCard.tsx +++ b/src/components/performance/PerformanceConfigCard.tsx @@ -24,6 +24,8 @@ import { import { savedOptIns, savedVersion, sortPresetsByTier } from '../../lib/performanceSelection'; import sqookyAvatar from '../../assets/performance-authors/sqooky.png'; import dacooderAvatar from '../../assets/performance-authors/dacooder.jpg'; +import eskayAvatar from '../../assets/performance-authors/eskay.png'; +import kaizuchanerusAvatar from '../../assets/performance-authors/kaizuchanerus.jpg'; import type { PerformanceConfigStatus, PerformanceLatestInfo, @@ -49,6 +51,8 @@ const SQOOKY_ARTIST: BrowseArtistRef = { const AUTHOR_AVATARS: Record = { Sqooky: sqookyAvatar, dacooder: dacooderAvatar, + Eskay: eskayAvatar, + kaizuchanerus: kaizuchanerusAvatar, }; interface Notice { diff --git a/src/lib/performanceSelection.test.ts b/src/lib/performanceSelection.test.ts index 16df0c08..6d75309c 100644 --- a/src/lib/performanceSelection.test.ts +++ b/src/lib/performanceSelection.test.ts @@ -50,7 +50,7 @@ function preset( const settings = (partial: Partial) => partial as AppSettings; describe('sortPresetsByTier', () => { - it('orders mildest to strongest with the preview config last', () => { + it('puts the recommended default first, then mildest to strongest with the preview config last', () => { const sorted = sortPresetsByTier([ preset('p', 'preview'), preset('m', 'maximum'), @@ -58,9 +58,11 @@ describe('sortPresetsByTier', () => { preset('pot', 'potato'), preset('a', 'aggressive'), preset('c', 'competitive'), + preset('l', 'light'), ]); expect(sorted.map((p) => p.tier)).toEqual([ 'balanced', + 'light', 'competitive', 'aggressive', 'maximum', diff --git a/src/lib/performanceSelection.ts b/src/lib/performanceSelection.ts index 061e5992..37ec8109 100644 --- a/src/lib/performanceSelection.ts +++ b/src/lib/performanceSelection.ts @@ -1,9 +1,11 @@ import type { AppSettings } from '../types/mod'; import type { PerformancePresetSummary } from '../types/electron'; -// Mildest to strongest, with the author's work-in-progress config last. +// The recommended default first, then mildest to strongest, with the author's +// work-in-progress config last. const TIER_ORDER: PerformancePresetSummary['tier'][] = [ 'balanced', + 'light', 'competitive', 'aggressive', 'maximum', diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 60257ea9..44b20333 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -3096,6 +3096,7 @@ "preset": { "authorMods": "Mods", "tier": { + "light": "Light", "balanced": "Balanced", "preview": "Preview", "competitive": "Competitive", @@ -3104,6 +3105,7 @@ "potato": "Potato" }, "tierBlurb": { + "light": "Close to Balanced, but keeps the full draw distance so buildings and players don't pop in.", "balanced": "A solid all-round gain that keeps the game looking close to normal.", "preview": "The author's in-progress tuning. Newer changes, less testing.", "competitive": "Tuned for ranked play: strips visual noise while keeping the game readable.", diff --git a/src/locales/manifest.json b/src/locales/manifest.json index f2287a80..63813407 100644 --- a/src/locales/manifest.json +++ b/src/locales/manifest.json @@ -1,6 +1,6 @@ { "sourceLanguage": "en", - "totalKeys": 2619, + "totalKeys": 2621, "languages": [ { "code": "bg", @@ -11,7 +11,7 @@ { "code": "en", "name": "English", - "translatedKeys": 2619, + "translatedKeys": 2621, "pct": 100 }, { diff --git a/src/types/electron.ts b/src/types/electron.ts index dd23aa6e..dfbb8de0 100644 --- a/src/types/electron.ts +++ b/src/types/electron.ts @@ -191,7 +191,7 @@ export interface PerformancePresetSummary { name: string; /** Upstream version of the newest bundled release, e.g. '2.8.2' or '4.2'. */ version: string; - tier: 'balanced' | 'preview' | 'aggressive' | 'potato' | 'competitive' | 'maximum'; + tier: 'light' | 'balanced' | 'preview' | 'aggressive' | 'potato' | 'competitive' | 'maximum'; author: string; /** Upstream itself labels this config experimental. */ unstable: boolean;