Skip to content

Latest commit

 

History

History
405 lines (317 loc) · 15.4 KB

File metadata and controls

405 lines (317 loc) · 15.4 KB

PaimonMultiplier — Development Plan

Dual-track plan covering Global Renaming (NahidaImpact → PaimonMultiplier) and Disguise System (Host-Tunneling entity morphing for mass-multiplayer).


Track A: Global Renaming — NahidaImpact → PaimonMultiplier

Scope Summary

Category Count Action
Project directories 6 Rename
.sln / .csproj files 7 Rename + rewrite refs
Namespace declarations ~110 Replace NahidaImpact.PaimonMultiplier.
using statements ~117 Replace NahidaImpact.PaimonMultiplier.
Type-alias using (Kcp) 12 Replace NahidaImpact.PaimonMultiplier.
String literals (console, email, title) 5 Targeted edits
Documentation (AGENTS.md, README.md) ~16 Targeted edits
Proto files (590+) 0 refs No change needed
Game asset JSON (ConfigAvatar_Nahida.json, icons, abilities) 90+ refs DO NOT RENAME — these reference the in-game character Nahida, not the project

Execution Order

Phase 1 — Rename directories and files on disk

mv NahidaImpact.sln              PaimonMultiplier.sln
mv NahidaImpact.Common           PaimonMultiplier.Common
mv NahidaImpact.Gameserver       PaimonMultiplier.Gameserver
mv NahidaImpact.Kcp              PaimonMultiplier.Kcp
mv NahidaImpact.Protocol         PaimonMultiplier.Protocol
mv NahidaImpact.Proxy            PaimonMultiplier.Proxy
mv NahidaImpact.SDK              PaimonMultiplier.SDK

Rename .csproj files within each directory:

NahidaImpact.Common/NahidaImpact.Common.csproj       → PaimonMultiplier.Common/PaimonMultiplier.Common.csproj
NahidaImpact.Gameserver/NahidaImpact.Gameserver.csproj → PaimonMultiplier.Gameserver/PaimonMultiplier.Gameserver.csproj
NahidaImpact.Kcp/NahidaImpact.Kcp.csproj             → PaimonMultiplier.Kcp/PaimonMultiplier.Kcp.csproj
NahidaImpact.Protocol/NahidaImpact.Protocol.csproj    → PaimonMultiplier.Protocol/PaimonMultiplier.Protocol.csproj
NahidaImpact.Proxy/NahidaImpact.Proxy.csproj          → PaimonMultiplier.Proxy/PaimonMultiplier.Proxy.csproj
NahidaImpact.SDK/NahidaImpact.SDK.csproj              → PaimonMultiplier.SDK/PaimonMultiplier.SDK.csproj

Phase 2 — Rewrite PaimonMultiplier.sln

Replace all 6 Project(...) entries: update project name strings and .csproj paths from NahidaImpact to PaimonMultiplier. Keep GUIDs unchanged.

Phase 3 — Update .csproj project references

File Old Reference New Reference
PaimonMultiplier.Common.csproj ..\NahidaImpact.Protocol\NahidaImpact.Protocol.csproj ..\PaimonMultiplier.Protocol\PaimonMultiplier.Protocol.csproj
PaimonMultiplier.Gameserver.csproj ..\NahidaImpact.Common\NahidaImpact.Common.csproj ..\PaimonMultiplier.Common\PaimonMultiplier.Common.csproj
PaimonMultiplier.Gameserver.csproj ..\NahidaImpact.Kcp\NahidaImpact.Kcp.csproj ..\PaimonMultiplier.Kcp\PaimonMultiplier.Kcp.csproj
PaimonMultiplier.Gameserver.csproj ..\NahidaImpact.Protocol\NahidaImpact.Protocol.csproj ..\PaimonMultiplier.Protocol\PaimonMultiplier.Protocol.csproj
PaimonMultiplier.SDK.csproj ..\NahidaImpact.Common\NahidaImpact.Common.csproj ..\PaimonMultiplier.Common\PaimonMultiplier.Common.csproj
PaimonMultiplier.SDK.csproj ..\NahidaImpact.Protocol\NahidaImpact.Protocol.csproj ..\PaimonMultiplier.Protocol\PaimonMultiplier.Protocol.csproj

Phase 4 — Global namespace + using replacement

Target: ALL .cs files across all 6 projects.

Single rule: Replace NahidaImpact.PaimonMultiplier. (trailing dot prevents matching game character "Nahida" in asset JSON).

This covers:

  • All namespace NahidaImpact.xxx; declarations
  • All using NahidaImpact.xxx; statements
  • All type-alias usings (e.g., NahidaImpact.Kcp.KcpBufferPaimonMultiplier.Kcp.KcpBuffer)
  • Cross-project type references

Phase 5 — Bare NahidaImpact string literals

File Old New
Gameserver/Program.cs line 20 Console.Title = "NahidaImpact | Game Server [Experimental] (4.3.50)" Console.Title = "PaimonMultiplier 4.3.50 - Infinite Coop Architecture"
Gameserver/Program.cs line 22 (ASCII art) NahidaImpact PaimonMultiplier
Proxy/Program.cs line 3 Console.Title = "NahidaImpact | Proxy" "PaimonMultiplier | Proxy"
SDK/Program.cs line 3 Console.Title = "NahidaImpact | SDK Server" "PaimonMultiplier | SDK Server"
SDK/Handlers/AuthHandler.cs line 20 Email = "NahidaImpact" "PaimonMultiplier"
SDK/Handlers/RegionHandler.cs line 41 Title = "NahidaImpact" "PaimonMultiplier"

Phase 6 — Documentation

AGENTS.md:

  • Line 1: # AGENTS.md — PaimonMultiplier
  • Line 6: dotnet build PaimonMultiplier.sln
  • Lines 10-11: project names
  • Lines 16-21: architecture table entries
  • Line 40: PaimonMultiplier.Common/assets/
  • Line 41: git remote URL

README.md:

  • Line 2: # PaimonMultiplier
  • Line 9: project names
  • Line 13: project name references

Phase 7 — Build verification

dotnet build PaimonMultiplier.sln

Track B: Disguise System — Host-Tunneling Entity Morphing

Overview

The Disguise System is the core architectural innovation enabling mass-multiplayer. Guest players are instantiated as ProtEntityType.Monster entities with EntityDisguiseInfo payloads, causing the 4.4.0 client to render them as full avatars. This bypasses the hardcoded 4-player team limit.

Phase 1 — Protocol: Create EntityDisguiseInfo.proto

New file: PaimonMultiplier.Protocol/EntityDisguiseInfo.proto

syntax = "proto3";

message EntityDisguiseInfo {
    uint32 avatar_id = 1;           // Visual avatar ID (e.g. 10000073 = Albedo)
    uint32 costume_id = 2;          // Costume override (0 = default)
    uint32 flycloak_id = 3;         // Glider visual (0 = default)
    uint32 skill_depot_id = 4;      // Ability visual effects
    uint32 weapon_id = 5;           // Visual weapon (0 = hide)
    uint32 weapon_costume_id = 6;   // Weapon skin override
}

Phase 2 — Protocol: Update SceneMonsterInfo.proto

Modify: PaimonMultiplier.Protocol/SceneMonsterInfo.proto

Add import and field:

import "EntityDisguiseInfo.proto";
// Inside SceneMonsterInfo message:
EntityDisguiseInfo entity_disguise_info = 24;

Field 24 is safe — the 4.2.5 proto zip shows fields 1-23 used.

Phase 3 — Entity: Create DisguisedMonsterEntity.cs

New file: PaimonMultiplier.Gameserver/Game/Entity/DisguisedMonsterEntity.cs

using NahidaImpact.Common.Constants;
using NahidaImpact.Common.Data.Binout;
using NahidaImpact.Common.Data.Binout.Ability;
using NahidaImpact.Common.Extensions;
using NahidaImpact.Gameserver.Game.Avatar;
using NahidaImpact.Protocol;

namespace NahidaImpact.Gameserver.Game.Entity;

internal class DisguisedMonsterEntity : SceneEntity
{
    public override ProtEntityType EntityType => ProtEntityType.Monster;

    public uint Uid { get; }
    public uint WeaponEntityId { get; }
    public GameAvatar GameAvatar { get; }

    public DisguisedMonsterEntity(GameAvatar gameAvatar, uint uid, uint entityId, uint weaponEntityId)
        : base(entityId)
    {
        Uid = uid;
        WeaponEntityId = ((uint)ProtEntityType.Weapon << 24) + weaponEntityId;
        GameAvatar = gameAvatar;
        Properties = gameAvatar.Properties;
        FightProperties = gameAvatar.FightProperties;
    }

    public AbilityControlBlock BuildAbilityControlBlock(BinDataCollection binData)
    {
        AbilityControlBlock block = new();
        AvatarConfig avatarConfig = binData.GetAvatarConfig(GameAvatar.AvatarId);

        uint defaultOverrideHash = "Default".GetStableHash();
        foreach (string abilityName in AvatarConstants.CommonAbilities)
        {
            block.AbilityEmbryoList.Add(new AbilityEmbryo
            {
                AbilityId = (uint)(block.AbilityEmbryoList.Count + 1),
                AbilityNameHash = abilityName.GetStableHash(),
                AbilityOverrideNameHash = defaultOverrideHash
            });
        }

        foreach (AbilityData ability in avatarConfig.Abilities)
        {
            block.AbilityEmbryoList.Add(new AbilityEmbryo
            {
                AbilityId = (uint)(block.AbilityEmbryoList.Count + 1),
                AbilityNameHash = ability.AbilityName.GetStableHash(),
                AbilityOverrideNameHash = ability.GetAbilityOverride().GetStableHash()
            });
        }

        return block;
    }

    public override SceneEntityInfo AsInfo()
    {
        SceneEntityInfo info = base.AsInfo();

        info.Monster = new SceneMonsterInfo
        {
            MonsterId = 210101001,
            ConfigId = 1,
            AuthorityPeerId = Uid,
            EntityDisguiseInfo = new EntityDisguiseInfo
            {
                AvatarId = GameAvatar.AvatarId,
                CostumeId = 0,
                FlycloakId = GameAvatar.WearingFlycloakId,
                SkillDepotId = GameAvatar.SkillDepotId,
                WeaponId = GameAvatar.WeaponId,
                WeaponCostumeId = 0
            }
        };

        return info;
    }
}

Design notes:

  • EntityType returns ProtEntityType.Monster (not Avatar)
  • AsInfo() populates SceneEntityInfo.monster with SceneMonsterInfo containing EntityDisguiseInfo
  • MonsterId 210101001 is a placeholder — any valid monster ID works since the client ignores it when EntityDisguiseInfo is present
  • Carries the same AbilityControlBlock pipeline as AvatarEntity for client-side ability effects
  • Shares MotionInfo, Properties, and FightProperties from the source GameAvatar

Phase 4 — Entity: Update EntityFactory.cs

Modify: PaimonMultiplier.Gameserver/Game/Entity/Factory/EntityFactory.cs

Add method:

public DisguisedMonsterEntity CreateDisguisedMonster(GameAvatar gameAvatar, uint belongUid)
{
    return new(gameAvatar, belongUid, ++_entityIdSeed, ++_entityIdSeed);
}

Phase 5 — Scene: Update SceneManager.cs

Modify: PaimonMultiplier.Gameserver/Game/Scene/SceneManager.cs

Add field and methods:

private readonly List<DisguisedMonsterEntity> _disguisedGuests = [];

public async ValueTask SpawnDisguisedGuestsAsync(List<GameAvatar> guestAvatars, uint guestUid)
{
    SceneEntity? hostEntity = _entityManager.GetEntityById(_player.CurAvatarEntityId);
    Vector pos = hostEntity?.MotionInfo.Pos ?? _player.LastMainWorldPos;

    foreach (GameAvatar guestAvatar in guestAvatars)
    {
        DisguisedMonsterEntity entity = _entityFactory.CreateDisguisedMonster(guestAvatar, guestUid);
        entity.SetPosition(pos.X, pos.Y, pos.Z);

        _disguisedGuests.Add(entity);
        await _entityManager.SpawnEntityAsync(entity, VisionType.Born);
    }
}

public async ValueTask RemoveDisguisedGuestsAsync()
{
    foreach (DisguisedMonsterEntity entity in _disguisedGuests)
    {
        await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify,
            new SceneEntityDisappearNotify
            {
                EntityList = { entity.EntityId },
                DisappearType = VisionType.Dismiss
            });
    }
    _disguisedGuests.Clear();
}

Update ResetState() to clear disguised guests:

private void ResetState()
{
    _teamAvatars.Clear();
    _disguisedGuests.Clear();
    _entityManager.Reset();
}

Phase 6 — Network: No changes needed

SessionEntityEventListener.OnEntitySpawned already broadcasts via CmdType.SceneEntityAppearNotify with entity.AsInfo(). The polymorphic dispatch ensures DisguisedMonsterEntity.AsInfo() produces the correct SceneMonsterInfo with EntityDisguiseInfo.

IEntityEventListener interface already handles generic SceneEntity — no modification required.

Phase 7 — Controller: New MultiplayerController.cs

New file: PaimonMultiplier.Gameserver/Controllers/MultiplayerController.cs

using NahidaImpact.Gameserver.Controllers.Attributes;
using NahidaImpact.Gameserver.Controllers.Result;
using NahidaImpact.Gameserver.Game;
using NahidaImpact.Gameserver.Game.Scene;
using NahidaImpact.Protocol;

namespace NahidaImpact.Gameserver.Controllers;

[NetController]
internal class MultiplayerController : ControllerBase
{
    [NetCommand(CmdType.GetWorldMpInfoReq)]
    public ValueTask<IResult> OnGetWorldMpInfoReq(Player player)
    {
        return ValueTask.FromResult(Response(CmdType.GetWorldMpInfoRsp, new GetWorldMpInfoRsp
        {
            IsMp = true,
            PlayerCount = 1
        }));
    }

    [NetCommand(CmdType.GetPlayerMpModeAvailabilityReq)]
    public ValueTask<IResult> OnGetPlayerMpModeAvailabilityReq()
    {
        return ValueTask.FromResult(Response(CmdType.GetPlayerMpModeAvailabilityRsp,
            new GetPlayerMpModeAvailabilityRsp
            {
                MpModeAvailability = 0
            }));
    }
}

Phase 8 — Build verification

dotnet build PaimonMultiplier.sln

Implementation Sequence

Both tracks must be executed in order because Track B code uses the OLD namespaces during development, then Track A renames everything.

Recommended order:

  1. Track B first — Implement Disguise System with current NahidaImpact namespaces
  2. Build and verify Track B works
  3. Track A — Global rename NahidaImpact → PaimonMultiplier
  4. Final build verification

Alternatively, if doing both simultaneously: rename first (Track A), then implement new code (Track B) using PaimonMultiplier namespaces from the start.


File Inventory

Track A Files Modified/Renamed

Operation File
Rename NahidaImpact.slnPaimonMultiplier.sln
Rename NahidaImpact.Common/PaimonMultiplier.Common/ (+ csproj)
Rename NahidaImpact.Gameserver/PaimonMultiplier.Gameserver/ (+ csproj)
Rename NahidaImpact.Kcp/PaimonMultiplier.Kcp/ (+ csproj)
Rename NahidaImpact.Protocol/PaimonMultiplier.Protocol/ (+ csproj)
Rename NahidaImpact.Proxy/PaimonMultiplier.Proxy/ (+ csproj)
Rename NahidaImpact.SDK/PaimonMultiplier.SDK/ (+ csproj)
Edit .sln — all 6 Project entries
Edit 4 .csproj files — ProjectReference paths
Edit ~110 .cs files — namespace + using replacements
Edit Gameserver/Program.cs — Console.Title + banner
Edit Proxy/Program.cs — Console.Title
Edit SDK/Program.cs — Console.Title
Edit SDK/Handlers/AuthHandler.cs — Email string
Edit SDK/Handlers/RegionHandler.cs — Title string
Edit AGENTS.md — all project references
Edit README.md — all project references

Track B Files Created/Modified

Operation File
Create Protocol/EntityDisguiseInfo.proto
Edit Protocol/SceneMonsterInfo.proto — add field 24
Create Gameserver/Game/Entity/DisguisedMonsterEntity.cs
Edit Gameserver/Game/Entity/Factory/EntityFactory.cs — add CreateDisguisedMonster
Edit Gameserver/Game/Scene/SceneManager.cs — add guest spawn/remove methods
Create Gameserver/Controllers/MultiplayerController.cs

Exclusions (DO NOT RENAME)

  • ConfigAvatar_Nahida.json — in-game character config (Nahida = Lesser Lord Kusanali)
  • AvatarExcelConfigData.jsonUI_AvatarIcon_Nahida, AvatarImage_Forward_Nahida (game assets)
  • All 590+ .proto files — no project name references
  • All appsettings.json files — no project name references