Skip to content

Feat: add game-specific server presets#22

Merged
FN-FAL113 merged 10 commits into
FN-FAL113:mainfrom
JanitorialMess:feat/server-presets
Apr 9, 2026
Merged

Feat: add game-specific server presets#22
FN-FAL113 merged 10 commits into
FN-FAL113:mainfrom
JanitorialMess:feat/server-presets

Conversation

@JanitorialMess

Copy link
Copy Markdown
Contributor

This PR adds the requested server presets feature (#17). I had to make a few assumptions you might not agree with, but I tried to keep the behavior intuitive and consistent with the existing block/unblock flow. Let me know if anything seems off or unclear. I should note that I did not test these changes on Linux.

Each preset stores:

  • the current game mode
  • whether the list is clustered or unclustered
  • the blocked server set for that view

Preset Application

Preset application is reset-first and does not diff against the current firewall state. Applying a preset clears the current server list for the active game/view, switches clustered state if needed, then applies the preset's blocked server set.

Dirty State

If the user manually blocks or unblocks servers after applying a preset, the app treats the state as dirty:

  • the active preset selection is cleared
  • the persisted last-selected preset for that game is cleared
  • Save Preset retains the previous preset name as an in-session suggestion, so saving back to the same preset doesn't require retyping the name

If the app is closed while dirty, no preset is auto-restored on next launch. It just loads the last selected game/view.

Clustered State

Clustered state is part of the persisted preset. A clustered preset restores the clustered view and blocked clusters; an unclustered preset restores the unclustered view and blocked individual servers. Manually toggling cluster/uncluster still follows the original reset-first behavior and clears the active preset selection, since switching representation discards the currently applied view-specific state.

Internal Reset Behavior / UnblockAllAsync()

The original code reused UnblockAllAsync() for internal flows like cluster/uncluster, but that method operates on FilteredServerModels. In practice, an internal reset can be silently limited by the active search filter, even though the surrounding code describes it as a full "unblock all" step.

The split now is:

  • the user-facing Unblock All action still operates on the filtered list
  • internal transition/reset paths now clear the full ServerModels collection

This prevents hidden stale rules from surviving cluster/uncluster transitions, game-mode switches, and preset apply resets. If that's not the original intent, I can revert that part independently.

Preset Saving

Preset saving uses in-session blocked-key tracking rather than reading firewall rules back from the OS:

  • firewall operations update an internal blocked-key set via TrackBlockedServerKeys(...)
  • saving serializes that set for the current game/view
  • applying a preset replaces the tracked set from the preset contents
  • saving with the currently selected/suggested preset name updates that preset directly and does not show an overwrite prompt
  • an overwrite confirmation appears when the entered name already exists for the current game and is different from the current preset

Deleting a preset only removes the saved entry. It does not modify currently applied firewall rules.

Performance

This PR does not depend on the firewall refactor (#21) and works correctly on current main.

It does however make the existing Windows firewall cost more visible, since preset apply, game switching, and cluster/uncluster transitions all go through reset-first rule changes.

I recorded a side-by-side comparison, left is this PR with the refactor, right is this PR on top of current main:

ServerPickerPresetsFirewallComp.mp4

Demo was recorded before I fixed the width change bug on the preset dropdown so don't mind that.

@Colovorat

Colovorat commented Mar 31, 2026

Copy link
Copy Markdown

Built it locally, works good, looks pretty good and intuitive, no comments on code, I'm just talking from perspective of a user. However save preset/delete preset buttons might clutter top bar too much? If there would be a need to add new buttons or ui elements those would need to move or something.
Otherwise thanks for the feature

Edit1:
image
also save/cancel buttons should change places, usually cancel is on the right i think :)

Edit2:
image
For example it already hides a refresh button on russian language

@JanitorialMess

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback! I'll swap the save/cancel button positions.

You're right about the clutter. I thought about a separate window like cs2-server-picker for managing blocked servers, but I like having everything in one place, especially since the clustering option in the main view is part of what gets saved in the preset. Moving the preset controls to a new row below the game search/selection/refresh is one option, though I'm not sold on how that looks:

ServerPickerX_AKmXt5Foq1

To avoid the refresh button overflow, I thought of a couple of options:

  • Increase the minimum width (I'd need to test which language produces the longest strings for that row)
  • Replace the text with a universal refresh icon (↺)
  • Split rows as suggested above

If you have any design suggestions, I'm all ears!

I also noticed that I missed an edge case around server list syncing/revisions. Right now the last selected preset is restored before revision sync runs. If the remote server list changed, the preset can be restored against outdated data first, and only afterward does the app reset the current game's rules and save the new revision.

Since server lists are lazy-loaded and only the current game's servers are available at a given time, I think I'll cleanup the presets only if a game is selected like so:

  • Reset the current rules for that game
  • Prune missing server keys from all presets for that game
  • Save the cleaned presets
  • Restore the last selected preset

I'll also need to update the sync message (SyncServersUnblockAllDialogue) so it reflects the preset cleanup/reapply path. Something along these lines:

Server data just got updated by Valve! All blocked servers will be unblocked in order to synchronize new server data. Preset entries for servers that no longer exist will be removed, and the last selected preset will be reapplied if available.

@Colovorat

Copy link
Copy Markdown

I thought about a separate window like cs2-server-picker for managing blocked servers

Yeah i thought about the same initially, drop down menu where one of the options would be to open a window to manage presets. But if you want to keep everything simple and in once place, you can replace safe/delete with icons of diskette and trash bin right next to the presets drop down. Seems intuitive that it's for save/delete when it's right next to the presets drop down

- Persist presets and last-selected preset names per game in JsonSetting
- Add ServerPresetModel plus preset name dialog for save/create flow
- Add preset picker and save/delete preset actions to the main window
- Store clustered state and blocked server keys in each preset
- Restore the last applied preset for the current game on load and game switch when the state is still clean
- Apply presets through a reset-first flow: clear current rules, switch clustered state if needed, then apply the preset's blocked set
- Track blocked server keys in-session so the current state can be saved without reading firewall rules back from the OS
- Clear the active preset selection and persisted last-selected preset when manual block/unblock or manual cluster changes dirty the current state
- Keep the last preset name as an in-session save suggestion so updating an existing preset does not require retyping it
- Use the full current ServerModels list for internal reset paths during preset apply, game switching, and cluster/uncluster transitions while keeping the user-facing Unblock All action filtered
- Update the cluster/uncluster button text after preset apply and manual view switches so it matches the actual active clustered state
- Add localized preset strings and a disabled empty-state UI when the current game has no presets
@FN-FAL113

FN-FAL113 commented Apr 1, 2026

Copy link
Copy Markdown
Owner

Think it would be better to move presets to a separate window with its own view model. If it requires access to the main window view model then just pass the view model as constructor parameter when initializing the window after clicking Presets button which can be placed in between cluster and refresh

@JanitorialMess

JanitorialMess commented Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

Sure, I'll move the preset controls to a separate window. Just want to clarify the scope:

Should the preset window be a simple management panel that operates on the main window's current state? As in:

  • Shows the list of saved presets for the current game
  • "Save" captures whatever is currently configured in the main window (blocked servers, clustered state)
  • "Apply" pushes a preset's state back to the main window (switches cluster view, applies blocks)
  • "Delete" removes a saved preset

The main window would keep the server list, cluster toggle, and block/unblock controls as-is, and just get a single "Presets" button that opens this window.

I'm asking because presets store the clustered/unclustered state and the blocked server set for that view. If the preset window needed its own server list, search, and cluster controls to let users build presets independently, it would end up being a near-duplicate of the main window. I'm assuming that's not the intent and that preset creation is just "configure your state in the main window, then save it as a preset."

Edit:
Now that I think more about it, we might need an independent server selection for the presets or we might have to start probing the actual firewall state and showing that in the main view. Separate columns (firewall status/ping) or highlighting blocked rows differently.
Currently the main view shows what server is reachable or not (since it pings it), not what server is intentionally blocked (the distinction is important).
I'm building the preset based on explicit "block" actions taken by the user, not the current server status. If say a server isn't responding, the user opens the app, sees "❌", assumes that that server is blocked (by intent) and clicks "Save Preset", it will not save anything in the block list of that preset. Because in reality the blocked/unblocked status isn't reflected in the view. A server not being reachable can be due to a few different factors ranging from connectivity issues, server being unstable/down, firewall settings, etc.

@FN-FAL113

Copy link
Copy Markdown
Owner

Sure, I'll move the preset controls to a separate window. Just want to clarify the scope:

Should the preset window be a simple management panel that operates on the main window's current state? As in:

  • Shows the list of saved presets for the current game
  • "Save" captures whatever is currently configured in the main window (blocked servers, clustered state)
  • "Apply" pushes a preset's state back to the main window (switches cluster view, applies blocks)
  • "Delete" removes a saved preset

The main window would keep the server list, cluster toggle, and block/unblock controls as-is, and just get a single "Presets" button that opens this window.

I'm asking because presets store the clustered/unclustered state and the blocked server set for that view. If the preset window needed its own server list and cluster controls to let users build presets independently, it would end up being a near-duplicate of the main window. I'm assuming that's not the intent and that preset creation is just "configure your state in the main window, then save it as a preset."

Edit: Now that I think more about it, we might need an independent server selection for the presets or we might have to start probing the actual firewall state and showing that in the main view. Separate columns (firewall status/ping) or highlighting blocked rows differently. Currently the main view shows what server is reachable or not (since it pings it), not what server is intentionally blocked (the distinction is important). I'm building the preset based on explicit "block" actions taken by the user, not the current server status. If say a server isn't responding, the user opens the app, sees "❌", assumes that that server is blocked (by intent) and clicks "Save Preset", it will not save anything in the block list of that preset. Because in reality the blocked/unblocked status isn't reflected in the view. A server not being reachable can be due to a few different factors ranging from connectivity issues, server being unstable/down, firewall settings, etc.

If you can make it similar to the cs2-server-picker style, where the presets window contains the CRUD operations with 2 datagrid, 1 for the preset names and the other is the preset servers.

@FN-FAL113

FN-FAL113 commented Apr 1, 2026

Copy link
Copy Markdown
Owner

If I were to approach this I'll just take the previous design from previous cs2 server picker app and convert it to a modern mvvm approach.

@JanitorialMess

Copy link
Copy Markdown
Contributor Author

I pushed some of the fixes we discussed earlier around preset pruning and a few behavior cleanups. I'll work on the new view when I can.

@Colovorat

Copy link
Copy Markdown

If I were to approach this I'll just take the previous design from previous cs2 server picker app and convert it to a modern mvvm approach.

Well if you are to take previous approach hopefully the separate window is just for managing presets, but the new dropdown for quick change of presets stays, because that is actually quite cool. Opening separate window to then pick if i want to block except preset, or block preset seems kinda like useless nesting.
I can imagine a window where you create a preset, pick if it's a blacklist or a whitelist(block except preset/block preset options), pick the servers, and save it. Then you choose said preset in the dropdown of the main window and that's it, no need to open separate window or anything. You just open the app, pick preset and done

@FN-FAL113

Copy link
Copy Markdown
Owner

If I were to approach this I'll just take the previous design from previous cs2 server picker app and convert it to a modern mvvm approach.

Well if you are to take previous approach hopefully the separate window is just for managing presets, but the new dropdown for quick change of presets stays, because that is actually quite cool. Opening separate window to then pick if i want to block except preset, or block preset seems kinda like useless nesting. I can imagine a window where you create a preset, pick if it's a blacklist or a whitelist(block except preset/block preset options), pick the servers, and save it. Then you choose said preset in the dropdown of the main window and that's it, no need to open separate window or anything. You just open the app, pick preset and done

Keep the dropdown in the main window. Separate window for managing presets to prevent a bit complicated UI/UX design.

@JanitorialMess

JanitorialMess commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

So here is what I settled on. I still need to iron out a few things before pushing. Any feedback?
You need to select which game you'll edit the preset of from the main view essentially. Then press "presets" and manage that game's presets in the new window.

Carnac_Gtf2sQV6W1.mp4

@Colovorat

Copy link
Copy Markdown

Yeah, that seems damn good. I'm curious if it's possible to add a whitelist option instead for a preset, but that seems to be out of scope. Otherwise looks good

@FN-FAL113

Copy link
Copy Markdown
Owner

So here is what I settled on. I still need to iron out a few things before pushing. Any feedback? You need to select which game you'll edit the preset of from the main view essentially. Then press "presets" and manage that game's presets in the new window.

Carnac_Gtf2sQV6W1.mp4

Looks good to me!

Comment thread ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs
@FN-FAL113

Copy link
Copy Markdown
Owner

So here is what I settled on. I still need to iron out a few things before pushing. Any feedback? You need to select which game you'll edit the preset of from the main view essentially. Then press "presets" and manage that game's presets in the new window.

Carnac_Gtf2sQV6W1.mp4

Sorry, this is not yet pushed right and still in WIP?

@JanitorialMess

Copy link
Copy Markdown
Contributor Author

So here is what I settled on. I still need to iron out a few things before pushing. Any feedback? You need to select which game you'll edit the preset of from the main view essentially. Then press "presets" and manage that game's presets in the new window.
Carnac_Gtf2sQV6W1.mp4

Sorry, this is not yet pushed right and still in WIP?

Sorry for the delay, just pushed the final changes (hopefully).
I tested the layout on all available languages and also added a GridSplitter between the two panels in case anyone wants to resize them (or if some new language addition messes things up down the line).

Sorting works on both the preset list and the blocked servers columns, country flags included. Had to write a custom NaturalStringComparer for preset name so something like "New Preset (10)" doesn't end up before "New Preset (1)".

I've tested as much as I could, let me know if I missed anything.

@FN-FAL113

Copy link
Copy Markdown
Owner

So here is what I settled on. I still need to iron out a few things before pushing. Any feedback? You need to select which game you'll edit the preset of from the main view essentially. Then press "presets" and manage that game's presets in the new window.
Carnac_Gtf2sQV6W1.mp4

Sorry, this is not yet pushed right and still in WIP?

Sorry for the delay, just pushed the final changes (hopefully). I tested the layout on all available languages and also added a GridSplitter between the two panels in case anyone wants to resize them (or if some new language addition messes things up down the line).

Sorting works on both the preset list and the blocked servers columns, country flags included. Had to write a custom NaturalStringComparer for preset name so something like "New Preset (10)" doesn't end up before "New Preset (1)".

I've tested as much as I could, let me know if I missed anything.

Great! I'll take time reviewing and will test it out on linux distros (Ubuntu, Arch Manjaro). Thanks.

@FN-FAL113 FN-FAL113 self-requested a review April 3, 2026 12:17

@FN-FAL113 FN-FAL113 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to extract PresetListItemViewModel and PresetServerSelectionItem inner class inside PresetManagerWindowViewModel.cs with their own dedicated model classes in Models directory

Also PresetListItemViewModel can be renamed to PresetItemModel since it doesn't get binded as context to a view and same rename for PresetServerSelectionItem to PresetServerItemModel? Correct me if I'm wrong with this.

@JanitorialMess

Copy link
Copy Markdown
Contributor Author

It might be better to extract PresetListItemViewModel and PresetServerSelectionItem inner class inside PresetManagerWindowViewModel.cs with their own dedicated model classes in Models directory

Also PresetListItemViewModel can be renamed to PresetItemModel since it doesn't get binded as context to a view and same rename for PresetServerSelectionItem to PresetServerItemModel? Correct me if I'm wrong with this.

I'm not an MVVM expert, but both classes are more like wrappers around existing domain models with their own UI state on top (IsEditing, IsDisplayVisible, IsBlocked, etc.) rather than plain data objects. The models they wrap already exist. For example, PresetListItemViewModel wraps ServerPresetModel and PresetServerSelectionItem wraps ServerModel.

PresetServerSelectionItem is definitely wrong though. Renaming it to PresetServerItemViewModel probably makes more sense. Let me know what you think, I can move the logic to /Models if you want.

@FN-FAL113

FN-FAL113 commented Apr 4, 2026

Copy link
Copy Markdown
Owner

It might be better to extract PresetListItemViewModel and PresetServerSelectionItem inner class inside PresetManagerWindowViewModel.cs with their own dedicated model classes in Models directory
Also PresetListItemViewModel can be renamed to PresetItemModel since it doesn't get binded as context to a view and same rename for PresetServerSelectionItem to PresetServerItemModel? Correct me if I'm wrong with this.

I'm not an MVVM expert, but both classes are more like wrappers around existing domain models with their own UI state on top (IsEditing, IsDisplayVisible, IsBlocked, etc.) rather than plain data objects. The models they wrap already exist. For example, PresetListItemViewModel wraps ServerPresetModel and PresetServerSelectionItem wraps ServerModel.

PresetServerSelectionItem is definitely wrong though. Renaming it to PresetServerItemViewModel probably makes more sense. Let me know what you think, I can move the logic to /Models if you want.

I'm not seeing outside references/usage for these three properties from PresetListItemViewModel. How are these utilized? Thanks

image

@JanitorialMess

JanitorialMess commented Apr 4, 2026

Copy link
Copy Markdown
Contributor Author

Sorry! That's deadcode. I had a selection bug for the preset rename and I tried to transform TextBlock->TextBox manually (outside of DataGrid lifecycle) to see if the edit field still bugged or not. I didn't fully remove the code tied to that.
IsEditing became _isEditingPresetName and StopEditingPresets is also unused.

For rename, the relevant code is: _allowPresetNameEdit, _isEditingPresetName, _editingPresetOriginalName, PresetListGrid.BeginEdit(). That being said, they are still wrappers around actual data objects used to drive the UI/view.

What do you think about splitting the logic into two classes PresetItemViewModel / PresetServerItemViewModel and renaming these entries?

  • PresetListItemViewModel -> PresetItemViewModel
  • PresetServerSelectionItem -> PresetServerItemViewModel

@FN-FAL113

FN-FAL113 commented Apr 4, 2026

Copy link
Copy Markdown
Owner

Sorry! That's deadcode. I had a selection bug for the preset rename and I tried to transform TextBlock->TextBox manually (outside of DataGrid lifecycle) to see if the edit field still bugged or not. I didn't fully remove the code tied to that. IsEditing became _isEditingPresetName and StopEditingPresets is also unused.

For rename, the relevant code is: _allowPresetNameEdit, _isEditingPresetName, _editingPresetOriginalName, PresetListGrid.BeginEdit(). That being said, they are still wrappers around actual data objects used to drive the UI/view.

What do you think about splitting the logic into two classes PresetItemViewModel / PresetServerItemViewModel and renaming these entries?

  • PresetListItemViewModel -> PresetItemViewModel
  • PresetServerSelectionItem -> PresetServerItemViewModel

I think its better splitting them into their own model classes since they are used as models inside PresetManagerWindowViewModel that datagrid controls bind to. Any unused code should be removed for maintainability and cleanliness.

@JanitorialMess

Copy link
Copy Markdown
Contributor Author

Just pushed the requested changes. Hopefully this is what you wanted.

@FN-FAL113

Copy link
Copy Markdown
Owner

Just pushed the requested changes. Hopefully this is what you wanted.

Thanks. I'll take time reviewing the changes since its a huge PR.

@FN-FAL113

Copy link
Copy Markdown
Owner

I refactored some stuffs for simplicity. Not sure if I broke something but I tried my best to test the new changes. If you have time, would appreciate a test to verify things still work as they should be.

@FN-FAL113 FN-FAL113 dismissed their stale review April 5, 2026 17:43

Resolved

@JanitorialMess

JanitorialMess commented Apr 6, 2026

Copy link
Copy Markdown
Contributor Author

I refactored some stuffs for simplicity. Not sure if I broke something but I tried my best to test the new changes. If you have time, would appreciate a test to verify things still work as they should be.

Found one last leftover unused reference. I tested the code with your changes, everything seems to work fine!

- Remove in-memory blocked-server tracking and simplify cluster/preset handling.
- Deleted the _blockedServerKeys field and TrackBlockedServerKeys/ReplaceBlockedServerKeysFromPreset methods; SetClusterStateAsync signature simplified (removed shouldUpdatePresetSelection) and callers updated.
- Preset selection is now marked dirty by clearing saved last-selected preset (MarkPresetSelectionDirtyAsync) and resetting SelectedPreset.
- Use JsonSetting methods directly for preset lookups (GetPresetsByGameMode/GetPresetByGameMode).
- Also normalize PerformOperationAsync calls to pass ServerModels directly.
@FN-FAL113 FN-FAL113 merged commit a51cf53 into FN-FAL113:main Apr 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants