Skip to content

Feature/offlevel party#718

Open
eduardosmaniotto wants to merge 42 commits intoMUnique:masterfrom
eduardosmaniotto:feature/offlevel-party
Open

Feature/offlevel party#718
eduardosmaniotto wants to merge 42 commits intoMUnique:masterfrom
eduardosmaniotto:feature/offlevel-party

Conversation

@eduardosmaniotto
Copy link
Copy Markdown
Contributor

@eduardosmaniotto eduardosmaniotto commented Mar 22, 2026

Offline Leveling Party Support

Summary
This PR adds party support for the offline leveling feature. It allows the offline leveling player to function within a party, receiving heals and buffs from party members.

New Features

  • Party Integration: Offline leveling now works with party members.
  • Pet Support: Added PetHandler for offline pet management.
  • Party Auto-Rejoin: Added PartyAutoRejoinPlugIn to automatically rejoin parties when reconnecting

Core Changes

  • OfflineLevelingPlayer: Now tracks party membership and can receive party-based heals/buffs.
  • BuffHandler: Extended to support party buff casting with proper range checks.
  • HealingHandler: Added party healing support.
  • CombatHandler: Added conditional skill support with timer-based and condition-based skill activation.
  • Added tests for all offline leveling handlers.

# Conflicts:
#	src/Persistence/Initialization/Updates/UpdateVersion.cs
# Conflicts:
#	src/GameLogic/GameContext.cs
#	src/GameLogic/OfflineLeveling/OfflineLevelingManager.cs
#	src/GameLogic/Properties/PlugInResources.resx
# Conflicts:
#	src/AttributeSystem/StatAttribute.cs
# Conflicts:
#	src/GameLogic/OfflineLeveling/OfflineLevelingManager.cs
# Conflicts:
#	src/GameLogic/OfflineLeveling/OfflineLevelingIntelligence.cs
#	src/GameLogic/OfflineLeveling/OfflineLevelingPlayer.cs
# Conflicts:
#	src/GameLogic/OfflineLeveling/OfflineLevelingIntelligence.cs
<ItemGroup Condition="'$(ci)'!='true'">
<PackageReference Include="BuildWebCompiler2022" />
</ItemGroup>
<ItemGroup Condition="'$(ci)'!='true' and '$(OS)'=='Windows_NT'">
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@sven-n I added this check as a QOL for Linux users, let me know if this could cause problems in Windows, I can remove.

<ProjectReference Include="..\ItemEditor\MUnique.OpenMU.Web.ItemEditor.csproj" />
</ItemGroup>

<Target Name="CompileSass" BeforeTargets="Build" Condition="'$(ci)'!='true' and '$(OS)'!='Windows_NT'">
Copy link
Copy Markdown
Contributor Author

@eduardosmaniotto eduardosmaniotto Mar 22, 2026

Choose a reason for hiding this comment

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

@sven-n Also a QOL feature for Linux. Build the assets with sass. It's ignored if its not installed.

@eduardosmaniotto
Copy link
Copy Markdown
Contributor Author

/gemini review

Copy link
Copy Markdown

@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

The pull request includes several changes and improvements. The lengths of the CashShopStorageListRequest and CashShopStorageItemConsumeRequest packets were updated, requiring updates to client implementations. The CurrentTarget property in BasicMonsterIntelligence was changed from protected to public, which could introduce unintended side effects. The HasEnoughResources method was added to CombatHandler to prevent casting skills without sufficient resources. Skill animation was added to HealingHandler before applying regeneration to party members. A new method IsAttackingPlayer was added to Monster to check if the monster is attacking a specific player. The ShouldApplyBuff method was introduced in BuffHandler to encapsulate the logic for determining whether a buff should be applied. The ExecuteAttackAsync method in CombatHandler now checks for configured buffs before attacking. The logic for refreshing the target in CombatHandler was refactored to use GetAttackableMonstersInHuntingRange. The combo skill check in CombatHandler was updated to ensure there are at least 3 combo skills configured. The description for ReflectedLightPink in docs/Packets/C1-11-ObjectHitExtended_by-server.md should consistently use 'reflected damage'. The ResolveTargetAsync method and IsCurrentTargetValid methods were introduced in BasicMonsterIntelligence to encapsulate target validation logic. The log messages in OfflineLevelingIntelligence and OfflineLevelingPlayer now use AccountLoginName instead of Name. The comment // todo: check the walk distance was removed from BasicMonsterIntelligence. The type of PathFinderPoolInstance was changed to IObjectPool<IPathFinder>. The ResetPathFinder method was added to PathFinder to clear the open list. The date format in Web/AdminPanel/Pages/LoggedIn.razor is hardcoded as yyyy-MM-dd HH:mm:ss.

/// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
/// </summary>
public static int Length => 8;
public static int Length => 10;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The length of the CashShopStorageListRequest packet is updated from 8 to 10. Ensure that all client implementations are updated accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

/// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
/// </summary>
public static int Length => 5;
public static int Length => 15;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The length of the CashShopStorageItemConsumeRequest packet is updated from 5 to 15. Ensure that all client implementations are updated accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

public IAttackable? CurrentTarget { get; private set; }

/// <inheritdoc/>
public void Start()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

By changing CurrentTarget from protected to public, you're exposing this property to external modification. Ensure this change doesn't introduce unintended side effects or security vulnerabilities.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider to revert this. Why is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted, there was no need to keep it public with the existing IsTargetingPlayer public method.

<ItemTemplate Context="item">
<td>@item.LoginName</td>
<td>@item.ServerId</td>
<td>@item.StartedAt.ToString("yyyy-MM-dd HH:mm:ss") UTC</td>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The date format is hardcoded as yyyy-MM-dd HH:mm:ss. Consider using a localized format or allowing the user to configure the format.

@eduardosmaniotto eduardosmaniotto marked this pull request as ready for review March 24, 2026 02:35
@gemini-code-assist
Copy link
Copy Markdown

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 significantly enhances the offline leveling system by integrating comprehensive party support, enabling offline players to seamlessly participate in parties, receive assistance from members, and manage their pets. It also introduces an auto-rejoin feature for parties and a more sophisticated conditional skill system for combat, improving the overall robustness and functionality of the offline leveling experience. Additionally, administrative oversight of offline sessions has been added to the Admin Panel.

Highlights

  • Offline Leveling Party Support: The offline leveling feature now fully supports party functionality, allowing offline players to receive heals and buffs from party members, and their status to be managed within the party system.
  • Pet Management for Offline Leveling: Introduced a new PetHandler to manage pets for offline leveling players, including initialization of Dark Raven behavior and checking pet durability.
  • Party Auto-Rejoin Mechanism: Implemented a PartyAutoRejoinPlugIn that automatically rejoins players to their previous party upon reconnecting to the game, ensuring party continuity.
  • Enhanced Conditional Skill System: The CombatHandler for offline leveling now supports conditional skill activation based on timers and specific combat conditions, allowing for more dynamic attack strategies.
  • Refactored Party and Player Data Models: Significant updates to IPartyManager, IPartyMember, Party, and Player classes to support offline party members and improve party management logic, including a new OfflinePartyMember class.
  • Admin Panel for Offline Leveling Sessions: The Admin Panel now displays active offline leveling sessions, allowing administrators to monitor and stop them as needed.
  • Packet Structure Updates: Updated packet definitions for ObjectHitExtended, ObjectHit, CashShopStorageListRequest, and CashShopStorageItemConsumeRequest to include new fields and correct lengths.

🧠 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.

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.

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
Copy Markdown

@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 significantly enhances the game's party and offline leveling systems. Key changes include the introduction of a new PartyManager and OfflinePartyMember to allow players to disconnect and reconnect to their parties, with offline members being represented by snapshots. The offline leveling (MuHelper) system has been extensively refactored to improve skill selection, buff application (including party buffs), pet control, and resource management, with new handlers for buffs, pets, and conditional skills. The admin panel now features a dedicated section to monitor and manage active offline leveling sessions. Additionally, packet definitions for cash shop requests have been updated, and numerous code quality improvements, such as structured logging, refined monster AI target resolution, and updated copyright headers, have been applied across the codebase.

Copy link
Copy Markdown
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.

Man, that's a huge PR. I just came to the half and wanted to submit the review, so you don't have to wait too long for feedback.

/// </summary>
/// <param name="player">The player to check.</param>
/// <returns>True if this monster is attacking the player; otherwise, false.</returns>
public bool IsAttackingPlayer(Player player)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this required?
If it's not needed, you can make CurrentTarget protected again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The bot has a feature, on conditional skills, to target monster attacking the player. Is there any other way to retrieve this information?
image

public IAttackable? CurrentTarget { get; private set; }

/// <inheritdoc/>
public void Start()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider to revert this. Why is this needed?

{
var strategy = this._player.GameContext.PlugInManager
.GetStrategy<short, ITargetedSkillPlugin>(skillEntry.Skill!.Number)
?? new TargetedSkillDefaultPlugin();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we cache the TargetedSkillDefaultPlugin in a class field?

Comment on lines +166 to +167
var buffs = this._buffHandler.ConfiguredBuffIds;
if (buffs.Any(id => id > 0))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is it even checking the buffs? When there is no skill, it can't attack anyway, can it?

Copy link
Copy Markdown
Contributor Author

@eduardosmaniotto eduardosmaniotto Mar 24, 2026

Choose a reason for hiding this comment

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

In this new version, if there are no buffs and no attack skills configured, the offline player uses normal attacks. If there are buffs configured, but no attack skills, it's treated as a buff specialized class, like EE, ASM or EBK for buffs only.

{
var strategy = this._player.GameContext.PlugInManager.GetStrategy<short, ITargetedSkillPlugin>((short)skill.Number)
var strategy = this._player.GameContext.PlugInManager.GetStrategy<short, ITargetedSkillPlugin>(skill.Number)
?? new TargetedSkillDefaultPlugin();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reuse a cached TargetedSkillDefaultPlugin?


var ids = this.GetConfiguredComboSkillIds();
if (ids.Count == 0)
if (ids.Count < 3)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

3 is a magic number here. Theoretically, a SkillComboDefinition can have more or less than 3 skills, too.
Add a named constant, at least.

await member.ApplyRegenerationAsync(this._player, healSkill).ConfigureAwait(false);

// Notify party of health change
await member.InvokeViewPlugInAsync<Views.Party.IUpdatePartyListPlugIn>(p => p.UpdatePartyListAsync()).ConfigureAwait(false);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't Party.HealthUpdateElapsed handle this already?

Copy link
Copy Markdown
Contributor Author

@eduardosmaniotto eduardosmaniotto Mar 24, 2026

Choose a reason for hiding this comment

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

Removed to implement the default behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are here so many changes? 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No clue what happened here, I'll revert this.

Comment on lines +162 to +163
? null
? string.Empty
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is it needed? It complicates some places which check the result with ??

Copy link
Copy Markdown
Contributor Author

@eduardosmaniotto eduardosmaniotto Mar 24, 2026

Choose a reason for hiding this comment

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

I updated because there were 2 failing tests, this is one of them. I'll adjusts the tests accordingly.

    /// Tests that <see cref="LocalizedString.GetTranslation(CultureInfo,bool)"/> returns an empty span when the underlying value is <see langword="null"/>.
    /// </summary>
    [Test]
    public void GetTranslation_ReturnsEmptySpan_WhenValueIsNull()
    {
        var localized = new LocalizedString(null!);

        var result = localized.GetTranslation(new CultureInfo("en"));

        Assert.IsEmpty(result);
    }

public string Name => this.SelectedCharacter?.Name ?? string.Empty;

/// <inheritdoc />
public Guid CharacterId => this.SelectedCharacter?.Id ?? Guid.Empty;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this needed? The character can be identified by name, it's unique, too.
My idea was to hide the Guids in game logic where possible, because that's an aspect of persistence.

Copy link
Copy Markdown
Contributor Author

@eduardosmaniotto eduardosmaniotto Mar 24, 2026

Choose a reason for hiding this comment

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

Refactored to use character name.

@eduardosmaniotto eduardosmaniotto requested a review from sven-n March 25, 2026 00:56
Copy link
Copy Markdown
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 reviewed the rest. This are the last things I found 😅

Comment on lines 67 to 71
}
catch (Exception ex)
{
this.Logger.LogError(ex, "Failed to initialize offline player for {Name}.", this.Name);
this.Logger.LogError(ex, "Failed to initialize offline player for {AccountLoginName}.", this.AccountLoginName);
return false;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we log this? The ToString returns account name and character name.
Alternatively, log to the Player.Log as it's specific to a player instance, too.

// All players in the range of the player are getting experience.
// This might not be like in the original server, where observing the killed monster counts,
// but at this stage, the monster already has cleared his observers.
this._distributionList.AddRange(this.PartyList.OfType<Player>().Where(p => p == killer || killer.Observers.Contains(p)));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please reintroduce the _distributionList. I added it to prevent allocations of new lists after every kill. As far as I remember, the _distributionLock is there to lock the access to this list.

/// </summary>
public sealed class PartyManager : IPartyManager
{
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, Party> _partyByCharacterName = new(StringComparer.OrdinalIgnoreCase);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

StringComparer.OrdinalIgnoreCase might be wrong. Afaik, the character name is case-sensitively unique, which means there can be a character test and Test on the same server.

@eduardosmaniotto eduardosmaniotto requested a review from sven-n March 26, 2026 23:02
@eduardosmaniotto
Copy link
Copy Markdown
Contributor Author

I'm not sure how to prevent Codacy warning on Dispose _healthUpdateCts even though it's already doing that.

Implement 'IDisposable' in this class and use the 'Dispose' method to call 'Dispose' on '_healthUpdateCts'.

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