Skip to content

Feature/offlevel#710

Open
eduardosmaniotto wants to merge 32 commits intoMUnique:masterfrom
eduardosmaniotto:feature/offlevel
Open

Feature/offlevel#710
eduardosmaniotto wants to merge 32 commits intoMUnique:masterfrom
eduardosmaniotto:feature/offlevel

Conversation

@eduardosmaniotto
Copy link
Contributor

@eduardosmaniotto eduardosmaniotto commented Mar 13, 2026

Feature: Offline Leveling (/offlevel command)
Allows players to continue grinding after disconnecting. When a player types /offlevel, their character stays in the world as a ghost and automatically fights monsters using the existing MU Helper configuration.
How it works:

  1. Player types /offlevel chat command
  2. The real client disconnects (player can reconnect anytime)
  3. A ghost player spawns at the same location and continues fighting using MU Helper
  4. When the player logs back in, the ghost automatically stops
    Technical details:
  • No network traffic to offline player: View plugin calls meant FOR the offline player are suppressed via NullViewProxy
  • Visible to other players: Attack animations are intentionally broadcast so other players can see the ghost fighting
  • Persists XP: Uses the same persistence context for the ghost, ensuring XP is saved

Feature: Add a "Refresh" button to admin interface to reload updated entities from the database.

Feature: Add a "Duplicate" button to inventory in admin interface.

PS. This also contains fixes from previous PR #706
PS2. This PR will not include party support, otherwise it would be too much to review.

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new feature: offline leveling. Players can now use a chat command to leave their character actively grinding in the game world even after disconnecting, leveraging their MU Helper settings. This is achieved through a 'ghost' player system that handles combat, item pickup, and movement, while minimizing network overhead. The feature also includes automatic session termination upon player re-login and refactors MU Helper configurations for clarity. Additionally, a quality-of-life improvement for administrators has been added, allowing for easy duplication of items within the admin panel.

Highlights

  • Offline Leveling Feature: Introduced a new /offlevel command that allows players to disconnect their client while their character remains in the game world as a 'ghost' player, automatically fighting monsters using their existing MU Helper configuration. This ghost player is visible to others and persists XP gains.
  • Network Optimization for Ghost Players: Implemented a NullViewProxy mechanism to suppress all network traffic for offline leveling ghost players, ensuring efficient server resource usage while maintaining their presence in the game world.
  • Automated AI for Offline Leveling: Developed an OfflineLevelingIntelligence component that mimics the client-side MU Helper logic, enabling ghost players to perform attacks, use skills, manage health, pick up items, and regroup to their original position.
  • Seamless Re-login Integration: Added a plugin (OfflineLevelingStopOnLoginPlugIn) that automatically stops an active offline leveling session when the real player logs back into their account, allowing them to resume control of their character.
  • MU Helper Configuration Refactoring: Refactored MU Helper configuration classes, renaming MuHelperConfiguration to MuHelperServerConfiguration and introducing MuHelperPlayerConfiguration to better distinguish between server-side and player-specific settings.
  • Admin Panel Item Duplication: Enhanced the admin panel's item storage field with a new 'Duplicate' button, allowing administrators to easily create copies of existing items.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/AttributeSystem/StatAttribute.cs
    • Updated null-conditional operator for Definition.MaximumValue.HasValue check.
  • src/GameLogic/DroppedItem.cs
    • Changed item pickup log level from Information to Debug.
  • src/GameLogic/DroppedMoney.cs
    • Changed money pickup log level from Information to Debug.
  • src/GameLogic/GameContext.cs
    • Added OfflineLevelingManager property to manage offline leveling sessions.
  • src/GameLogic/GameMap.cs
    • Added GetDropsInRange method to retrieve dropped items and money within a specified range.
  • src/GameLogic/IGameContext.cs
    • Added OfflineLevelingManager property to the game context interface.
  • src/GameLogic/MuHelper/MuHelper.cs
    • Updated _configuration type from MuHelperConfiguration to MuHelperServerConfiguration.
  • src/GameLogic/MuHelper/MuHelperConfiguration.cs
    • Renamed MuHelperConfiguration.cs to MuHelperServerConfiguration.cs.
  • src/GameLogic/MuHelper/MuHelperFeaturePlugIn.cs
    • Updated configuration type from MuHelperConfiguration to MuHelperServerConfiguration.
  • src/GameLogic/MuHelper/MuHelperPlayerConfiguration.cs
    • Added new file defining the deserialized structure for player-specific MU Helper configurations.
  • src/GameLogic/OfflineLeveling/NullViewPlugInContainer.cs
    • Added new file for a view plugin container that creates NullViewProxy instances to suppress network traffic.
  • src/GameLogic/OfflineLeveling/NullViewProxy.cs
    • Added new file for a dynamic proxy that returns default values for method calls, effectively nullifying view plugin operations.
  • src/GameLogic/OfflineLeveling/OfflineLevelingIntelligence.cs
    • Added new file implementing the AI logic for offline leveling ghost players, including combat, healing, item pickup, and movement.
  • src/GameLogic/OfflineLeveling/OfflineLevelingManager.cs
    • Added new file to track and manage active offline leveling player instances.
  • src/GameLogic/OfflineLeveling/OfflineLevelingPlayer.cs
    • Added new file defining the OfflineLevelingPlayer class, representing a ghost player for offline leveling.
  • src/GameLogic/OfflineLeveling/OfflineLevelingStopOnLoginPlugIn.cs
    • Added new file for a plugin that stops an active offline leveling session when the associated account logs in.
  • src/GameLogic/Player.cs
    • Added SuppressDisconnectedEvent method to prevent event firing during offline leveling setup.
    • Removed trailing newline at the end of the file.
  • src/GameLogic/PlugIns/ChatCommands/Arguments/OfflineLevelingChatCommandPlugIn.cs
    • Added new file for the /offlevel chat command, enabling players to start offline leveling.
  • src/GameLogic/Properties/PlayerMessage.Designer.cs
    • Added new localized strings for offline leveling messages.
  • src/GameLogic/Properties/PlayerMessage.resx
    • Added new resource entries for offline leveling messages.
  • src/GameLogic/Properties/PlugInResources.Designer.cs
    • Added new localized strings for offline leveling plugin names and descriptions.
  • src/GameLogic/Properties/PlugInResources.resx
    • Added new resource entries for offline leveling plugin names and descriptions.
  • src/Persistence/Initialization/Updates/AddOfflineLevelingPlugIn.cs
    • Added new file to register the offline leveling chat command and stop-on-login plugins during database initialization.
  • src/Persistence/Initialization/Updates/UpdateVersion.cs
    • Added AddOfflineLeveling entry to the UpdateVersion enum.
  • src/Startup/Dockerfile
    • Added icu-libs installation and DOTNET_SYSTEM_GLOBALIZATION_INVARIANT environment variable for globalization support.
    • Refined Dockerfile build and publish steps, including better handling of project files and log directory creation.
  • src/Web/Shared/Components/Form/ItemStorageField.razor
    • Added a 'Duplicate' button to the item storage field in the admin panel.
  • src/Web/Shared/Components/Form/ValueListWrapper.cs
    • Modified Remove and RemoveAt methods to correctly handle item removal and index updates within the wrapper.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant new feature: offline leveling. The implementation is comprehensive, covering the AI for the offline character, state management, and integration with existing systems. The use of DispatchProxy to suppress network views is a clever approach. Additionally, the PR includes some refactoring and a new item duplication feature in the admin UI. My review identified a few areas for improvement: a minor file header inconsistency, a maintainability issue with a hardcoded string for skill identification, and a potential bug in the new item duplication logic that could lead to data issues. I've provided suggestions for each of these points.

@eduardosmaniotto eduardosmaniotto marked this pull request as draft March 13, 2026 15:24
@eduardosmaniotto eduardosmaniotto marked this pull request as ready for review March 14, 2026 00:40
@eduardosmaniotto eduardosmaniotto marked this pull request as draft March 14, 2026 00:40
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant new feature: offline leveling. The implementation is comprehensive, including a ghost player entity, a server-side AI to control it, and plugins to manage its lifecycle. The approach to suppress network traffic for the ghost player using a null-view proxy is clever. Additionally, the PR includes UI enhancements like a 'Duplicate' button for items in the admin interface. While the core feature implementation is solid, I've identified a critical bug in the new 'Duplicate' item functionality that could lead to data corruption, along with a couple of medium-severity maintainability concerns in the new code that would be beneficial to address.

@eduardosmaniotto eduardosmaniotto marked this pull request as ready for review March 14, 2026 03:05
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the significant 'Offline Leveling' feature, allowing players to continue grinding after disconnecting. The implementation is comprehensive, including the AI for the offline player, management of sessions, and seamless integration with the login process. The use of a DispatchProxy to suppress network traffic for offline players is a clever approach. Additionally, the PR adds a 'Duplicate' item feature in the admin panel's inventory editor and a 'Refresh' button on edit pages. Several smaller refactorings improve code style and robustness. My review includes a couple of suggestions to enhance maintainability by refactoring large classes and better separating business logic from UI components. Overall, this is a solid contribution with well-executed features.

@eduardosmaniotto
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant new feature: offline leveling. The implementation is well-structured, creating a 'ghost' player driven by a server-side AI that mimics the MU Helper functionality. The use of a null-view proxy to suppress network traffic is a clever approach. The supporting changes, such as the admin panel refresh and duplicate item buttons, are also well-implemented. I've found one potential issue regarding the game's economy that should be considered.

@eduardosmaniotto eduardosmaniotto marked this pull request as draft March 14, 2026 04:32
@eduardosmaniotto eduardosmaniotto marked this pull request as ready for review March 14, 2026 20:23
@eduardosmaniotto eduardosmaniotto marked this pull request as draft March 16, 2026 17:59
@eduardosmaniotto eduardosmaniotto marked this pull request as ready for review March 16, 2026 18:37
@eduardosmaniotto eduardosmaniotto requested a review from sven-n March 16, 2026 18:37
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the impressive offline leveling feature, allowing players to continue grinding after they disconnect. The implementation is comprehensive, creating a 'ghost' player that mimics the MU Helper's behavior through a well-structured set of new classes. The use of a NullViewPlugInContainer to suppress network traffic is a clever approach. The login logic has been thoughtfully refactored to handle session handovers from offline players. Additionally, the new 'Refresh' and 'Duplicate' buttons in the admin panel are welcome usability improvements. The overall code quality is high, with good attention to details like concurrency and resource management. I have one minor suggestion to enhance robustness.

Copy link
Member

@sven-n sven-n left a comment

Choose a reason for hiding this comment

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

Looks almost perfect now!
I didn't review the new LoginAction yet, I need a clear mind for that, hopefully tomorrow 😅

@eduardosmaniotto eduardosmaniotto requested a review from sven-n March 18, 2026 00:26
@eduardosmaniotto
Copy link
Contributor Author

Looks almost perfect now! I didn't review the new LoginAction yet, I need a clear mind for that, hopefully tomorrow 😅

Thanks. It was basically a code extraction with the addition of this lines:

            var finalAccount = account;
            if (player.GameContext.OfflineLevelingManager.IsActive(username))
            {
                finalAccount = await this.HandleOfflineSessionHandoverAsync(player, username, password, account).ConfigureAwait(false);
                if (finalAccount is null)
                {
                    context.Allowed = false;
                    return (false, null);
                }
            }
    private async ValueTask<Account?> HandleOfflineSessionHandoverAsync(Player player, string username, string password, Account account)
    {
        player.Logger.LogInformation("Account {username} has an active offline session. Stopping it and reloading account data.", username);
        await player.GameContext.OfflineLevelingManager.StopAsync(username).ConfigureAwait(false);
        player.PersistenceContext.Detach(account);
        var reloadedAccount = await player.PersistenceContext.GetAccountByLoginNameAsync(username, password).ConfigureAwait(false);
        if (reloadedAccount is null)
        {
            player.Logger.LogError("Failed to reload account {username} after stopping offline session.", username);
            await player.InvokeViewPlugInAsync<IShowLoginResultPlugIn>(p => p.ShowLoginResultAsync(LoginResult.ConnectionError)).ConfigureAwait(false);
        }

        return reloadedAccount;
    }

The plugin was causing some persistence issues, so I deleted it and moved here.

@sven-n
Copy link
Member

sven-n commented Mar 18, 2026

@gemini-code-assist can you take a closer look at the changes of LoginAction?

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive "Offline Leveling" feature, enabling player characters to continue progression while disconnected, managed by new AI components for combat, buffing, healing, item pickup, movement, and Zen consumption. Key changes include refactoring the login process to handle offline sessions, improving concurrency for player management with ConcurrentDictionary and TryAdd/TryRemove methods, and adjusting logging levels for item/money events to debug. The admin panel now features a refresh button, and the item editor has been enhanced with item duplication and improved item placement logic. A critical issue was identified where an exception during account data reloading after an offline session handover was not logged, which could mask underlying problems.

Copy link
Member

@sven-n sven-n left a comment

Choose a reason for hiding this comment

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

Okay, I had to think a bit until I understood this new logic in the LoginAction. I'm sure it works and a lot of considerations went into it, but it should be improved. player.PersistenceContext.GetAccountByLoginNameAsync is now called twice when offline leveling is involved, and it can actually be a very resource intensive call on the database as it retrieves the whole account data with one big json query.

I think we need a new AccountState? AuthenticateAccountAsync(username, password) function on the persistence context which just retrieves the necessary data and returns just the account state, or null if password is wrong. The flag, that an account is a template shouldn't change between logins of an offline leveling player, imho. So, this information can be taken from the already online OfflineLevelingPlayer.

In the LoginAction we first call the new auth function, then

  • If it's an offline leveling player, just load the full account again after the offline manager saved it, not before.
  • If it's not an offline leveling player, load the account, right after that, of course.
    Both can then be done over GetAccountByLoginNameAsync(userName) without checking the password again.

Can you change this, please? 😅

@eduardosmaniotto
Copy link
Contributor Author

eduardosmaniotto commented Mar 18, 2026

Okay, I had to think a bit until I understood this new logic in the LoginAction. I'm sure it works and a lot of considerations went into it, but it should be improved. player.PersistenceContext.GetAccountByLoginNameAsync is now called twice when offline leveling is involved, and it can actually be a very resource intensive call on the database as it retrieves the whole account data with one big json query.

I think we need a new AccountState? AuthenticateAccountAsync(username, password) function on the persistence context which just retrieves the necessary data and returns just the account state, or null if password is wrong. The flag, that an account is a template shouldn't change between logins of an offline leveling player, imho. So, this information can be taken from the already online OfflineLevelingPlayer.

In the LoginAction we first call the new auth function, then

  • If it's an offline leveling player, just load the full account again after the offline manager saved it, not before.
  • If it's not an offline leveling player, load the account, right after that, of course.
    Both can then be done over GetAccountByLoginNameAsync(userName) without checking the password again.

Can you change this, please? 😅

No problem, all improvement suggestions are welcome.

  • Added AuthenticateAsync(loginName, password) to IPlayerContext, only fetches PasswordHash and State.
  • Added TryGetPlayer to OfflineLevelingManager to access the live OfflineLevelingPlayer and read IsTemplate from it without a DB call.
  • Refactored LoginAction flow:
    • Authenticate first via AuthenticateAsync
    • Validate ban state from the result
    • For offline sessions: read IsTemplate from the live offline player → stop session → load full account once via GetAccountByLoginNameAsync(username) (no password)
    • For normal sessions: load full account once via GetAccountByLoginNameAsync(username) (no password) → read IsTemplate from it

@eduardosmaniotto eduardosmaniotto requested a review from sven-n March 18, 2026 23:27
1. 🔴 Login server session leak on offline handover failure

In TryEstablishSessionAsync(Player, string), when an offline session is active for a non-template account, TryLoginAsync(string, byte) registers the session on the login server before HandleOfflineSessionHandoverAsync(Player, string) is called. If the handover returns null (e.g., account reload fails), the method returns (false, null) but never calls LogOffAsync() — the account is permanently stuck as "connected" on the login server, blocking all future logins.

2. 🔴 Login server session leak when FinishLoginAsync(Player, string, Account) throws

In LoginAsync(Player, string, string), after TryEstablishSessionAsync(Player, string) succeeds (which registers the session on the login server and advances the player state), FinishLoginAsync(Player, string, Account) is called without a try/catch. If it throws (e.g., ShowLoginResultAsync(LoginResult) or InvokeViewPlugInAsync<TViewPlugIn>(this IWorldObserver, Func<TViewPlugIn, ValueTask>) fails), the login server session is never cleaned up.

3. 🟡 TOCTOU race between IsActive(string) and TryGetPlayer(string, out OfflineLevelingPlayer?)

IsActive(username) and TryGetPlayer(username, ...) are two separate calls on a ConcurrentDictionary. Between them, another thread could call StopAsync(), removing the entry. This means the code enters the offline branch but TryGetPlayer(string, out OfflineLevelingPlayer?) returns false, making isTemplateOffline = false. This is mostly benign — it just means a non-template login flow is used as fallback — but could cause a redundant StopAsync() call in HandleOfflineSessionHandoverAsync(Player, string) and an unnecessary TryLoginAsync(string, byte) call. A single TryGetPlayer(string, out OfflineLevelingPlayer?) call can replace both:

4. 🟡 Allowed not set to false on some failure paths

On line 109, when player.GameContext is not IGameServerContext, the method returns (false, null) but Allowed is not set to false. When the Context is disposed, the state machine may still advance to Authenticated even though the login failed.

Additional observation:
catch in TryEstablishSessionAsync(Player, string) (line 158–163): If TryLoginAsync(string, byte) succeeded before the exception, the outer catch doesn't call LogOffAsync(). However, tracking whether the login server session was established within this broad catch would add significant complexity. The fix for issue MUnique#2 above covers the most likely post-login failure path (FinishLoginAsync(Player, string, Account)).
Copy link
Member

@sven-n sven-n left a comment

Choose a reason for hiding this comment

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

Okay, I let claude review the LoginAction and it found some stuff - I committed the fixes, I'm clearly impressed...

Then I tested it and found some things:

  1. Undefined behavior: Should an active offlevel template account stay online when it connects again? I'd say yes, but what do you think? Currently, the offlevel template account is removed from the game when it logs in again.
  2. Bug?: The raven attack doesn't seem to be working during offlevel. Probably a missing initialization at Player.PetCommandManager when offline leveling is started.
  3. Bug: When the offlevel player dies, it'll not respawn in the safezone and not removed from the OfflineLevelingManager. It's stuck in the respawn logic, because the server expects a ClientReadyAfterMapChange message from the client. The OfflineLevelingPlayer class should call Player.ClientReadyAfterMapChangeAsync somehow. Maybe also a specific IRespawnAfterDeathPlugIn could do that.

@eduardosmaniotto
Copy link
Contributor Author

eduardosmaniotto commented Mar 21, 2026

Okay, I let claude review the LoginAction and it found some stuff - I committed the fixes, I'm clearly impressed...

Then I tested it and found some things:

  1. Undefined behavior: Should an active offlevel template account stay online when it connects again? I'd say yes, but what do you think? Currently, the offlevel template account is removed from the game when it logs in again.
  2. Bug?: The raven attack doesn't seem to be working during offlevel. Probably a missing initialization at Player.PetCommandManager when offline leveling is started.
  3. Bug: When the offlevel player dies, it'll not respawn in the safezone and not removed from the OfflineLevelingManager. It's stuck in the respawn logic, because the server expects a ClientReadyAfterMapChange message from the client. The OfflineLevelingPlayer class should call Player.ClientReadyAfterMapChangeAsync somehow. Maybe also a specific IRespawnAfterDeathPlugIn could do that.
  1. I think yes too, my understanding from template accounts is that they are read-only so they don't need most of the safeguards that the current offline command requires. But for a bot feature I would prefer to handle them on the admin page, like configuring which character, map, and coordinates to spawn, maybe even a batch add or reusing the same template character, and not worry about logging in on them at all. I actually have some ideas for a bot feature, like implementing a /request auto or /reauto that some private server have, which allows another player to join a party automatically, this way on the server, we could add some bots on each map that new players could join and hunt together. Not sure if it's a good idea, but it's possible.
  2. I'ts a missing feature in this branch. There are multiple stuff missing in this one that I've implemented in a separate branch together with party support. I'll open a new PR once this one is ready to go.
  3. I've implemented a fix, with a OfflineViewPlugInContainer that handles respawn and map change events.

@eduardosmaniotto eduardosmaniotto requested a review from sven-n March 21, 2026 01:30
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.

2 participants