yuanshen/Yuanshen.Gameserver/Game/SceneManager.cs
wisedev fc2ac1050f First working version of the server
KCP gameserver with packet dispatch, scene streaming, combat, gacha and the
shop, plus a small HTTP server for the login flow and the wish history page.

Game data under assets is pulled out of the client instead of being typed by
hand. The excel tables come from the packed bin container and the spawn points
from the compiled scene scripts.
2026-09-04 02:10:50 +03:00

247 lines
8.7 KiB
C#

using Yuanshen.Gameserver.Network.Session;
using Yuanshen.Protocol;
namespace Yuanshen.Gameserver.Game;
internal sealed class SceneManager
{
private const int LevelSyncAttempts = 3;
private readonly NetSession _session;
private readonly Player _player;
private readonly GameState _state;
public SceneManager(NetSession session, Player player, GameState state)
{
_session = session;
_player = player;
_state = state;
}
public SceneTeamUpdateNotify BuildSceneTeamUpdate()
{
SceneTeamUpdateNotify notify = new();
foreach (GameAvatar avatar in _player.TeamAvatars)
{
GameWeapon weapon = _player.FindWeapon(avatar.WeaponGuid)!;
notify.SceneTeamAvatarList.Add(new SceneTeamAvatar
{
PlayerId = _player.Uid,
AvatarGuid = avatar.Guid,
SceneId = GameState.SceneId,
EntityId = avatar.EntityId,
WeaponGuid = avatar.WeaponGuid,
WeaponEntityId = avatar.WeaponEntityId,
IsPlayerCurAvatar = avatar.Guid == _player.CurrentAvatarGuid,
IsOnScene = avatar.Guid == _player.CurrentAvatarGuid,
AbilityControlBlock = avatar.BuildAbilityControlBlock(),
AvatarAbilityInfo = new AbilitySyncStateInfo { IsInited = true },
WeaponAbilityInfo = new AbilitySyncStateInfo { IsInited = true },
SceneEntityInfo = avatar.AsSceneEntity(_state.CurrentPos, weapon, EquippedReliquaries(avatar))
});
}
return notify;
}
public PlayerEnterSceneInfoNotify BuildEnterSceneInfo()
{
GameAvatar active = _player.ActiveAvatar!;
PlayerEnterSceneInfoNotify notify = new()
{
CurAvatarEntityId = active.EntityId,
EnterSceneToken = _state.EnterSceneToken,
MpLevelEntityInfo = new MPLevelEntityInfo
{
EntityId = Player.MpLevelEntityId,
AuthorityPeerId = Player.PeerId,
AbilityInfo = new AbilitySyncStateInfo { IsInited = true }
},
TeamEnterInfo = new TeamEnterSceneInfo
{
TeamEntityId = Player.TeamEntityId,
TeamAbilityInfo = new AbilitySyncStateInfo { IsInited = true }
}
};
foreach (GameAvatar avatar in _player.TeamAvatars)
{
notify.AvatarEnterInfo.Add(new AvatarEnterSceneInfo
{
AvatarGuid = avatar.Guid,
AvatarEntityId = avatar.EntityId,
WeaponGuid = avatar.WeaponGuid,
WeaponEntityId = avatar.WeaponEntityId,
AvatarAbilityInfo = new AbilitySyncStateInfo { IsInited = true },
WeaponAbilityInfo = new AbilitySyncStateInfo { IsInited = true }
});
}
return notify;
}
public async ValueTask SpawnActiveAvatarAsync(VisionType visionType)
{
GameAvatar active = _player.ActiveAvatar!;
GameWeapon weapon = _player.FindWeapon(active.WeaponGuid)!;
await _session.NotifyAsync(CmdType.SceneEntityAppearNotify, new SceneEntityAppearNotify
{
AppearType = visionType,
EntityList = { active.AsSceneEntity(_state.CurrentPos, weapon, EquippedReliquaries(active)) }
});
}
public async ValueTask DespawnMonsterAsync(GameMonster monster)
{
_player.Monsters.Remove(monster);
SceneEntityDisappearNotify notify = new() { DisappearType = VisionType.VisionRemove };
notify.EntityList.Add(monster.EntityId);
await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, notify);
}
public async ValueTask AppearMonsterAsync(GameMonster monster)
{
SceneEntityAppearNotify notify = new() { AppearType = VisionType.VisionBorn };
notify.EntityList.Add(monster.AsSceneEntity());
await _session.NotifyAsync(CmdType.SceneEntityAppearNotify, notify);
}
public async ValueTask SyncMonsterLevelAsync(GameMonster monster)
{
await _session.NotifyAsync(CmdType.EntityPropNotify, new EntityPropNotify
{
EntityId = monster.EntityId,
PropMap = { { 4001u, new PropValue { Type = 4001, Ival = monster.Level, Val = monster.Level } } }
});
}
public async ValueTask SyncPendingMonsterLevelsAsync()
{
foreach (GameMonster monster in _player.Monsters.Where(m => m.IsAlive && m.LevelSyncCount < LevelSyncAttempts).ToList())
{
monster.LevelSyncCount++;
await SyncMonsterLevelAsync(monster);
}
}
public async ValueTask ClearMonstersAsync()
{
if (_player.Monsters.Count == 0)
return;
SceneEntityDisappearNotify notify = new() { DisappearType = VisionType.VisionRemove };
foreach (GameMonster monster in _player.Monsters)
notify.EntityList.Add(monster.EntityId);
_player.Monsters.Clear();
await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, notify);
}
public async ValueTask TeleportAsync(Vector pos)
{
_state.UpdatePosition(pos);
await _session.NotifyAsync(CmdType.PlayerEnterSceneNotify, new PlayerEnterSceneNotify
{
SceneId = GameState.SceneId,
Pos = pos,
SceneBeginTime = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Type = EnterType.EnterGoto,
TargetUid = _player.Uid,
EnterSceneToken = _state.NextEnterSceneToken(),
WorldLevel = 0,
EnterReason = 1
});
}
public async ValueTask HealTeamAsync()
{
foreach (GameAvatar avatar in _player.TeamAvatars)
await _session.NotifyAsync(CmdType.EntityFightPropUpdateNotify, new EntityFightPropUpdateNotify
{
EntityId = avatar.EntityId,
FightPropMap = { { 1010u, 999999f }, { 2000u, 999999f } }
});
}
public async ValueTask RefreshAvatarDataAsync()
{
AvatarDataNotify data = new()
{
CurAvatarTeamId = _player.CurrentTeamId,
ChooseAvatarGuid = _player.CurrentAvatarGuid
};
foreach (uint flycloak in Player.Flycloaks)
data.OwnedFlycloakList.Add(flycloak);
foreach (GameAvatar avatar in _player.Avatars)
data.AvatarList.Add(avatar.AsAvatarInfo());
AvatarTeam team = new() { TeamName = "Team " + _player.CurrentTeamId };
team.AvatarGuidList.AddRange(_player.TeamAvatarGuids);
data.AvatarTeamMap.Add(_player.CurrentTeamId, team);
await _session.NotifyAsync(CmdType.AvatarDataNotify, data);
}
public async ValueTask RefreshTeamAsync()
=> await _session.NotifyAsync(CmdType.SceneTeamUpdateNotify, BuildSceneTeamUpdate());
public async ValueTask SetupTeamAsync(uint teamId, IReadOnlyList<ulong> guids, ulong curAvatarGuid)
{
ulong previousActiveGuid = _player.CurrentAvatarGuid;
_player.CurrentTeamId = teamId;
_player.TeamAvatarGuids.Clear();
_player.TeamAvatarGuids.AddRange(guids);
_player.CurrentAvatarGuid = curAvatarGuid != 0 ? curAvatarGuid : guids[0];
AvatarTeam team = new() { TeamName = "Team " + teamId };
team.AvatarGuidList.AddRange(guids);
await _session.NotifyAsync(CmdType.AvatarTeamUpdateNotify, new AvatarTeamUpdateNotify
{
AvatarTeamMap = { { teamId, team } }
});
if (previousActiveGuid != _player.CurrentAvatarGuid)
await DisappearAsync(previousActiveGuid, VisionType.VisionReplace);
await _session.NotifyAsync(CmdType.SceneTeamUpdateNotify, BuildSceneTeamUpdate());
if (previousActiveGuid != _player.CurrentAvatarGuid)
await SpawnActiveAvatarAsync(VisionType.VisionReplace);
}
public async ValueTask ChangeAvatarAsync(ulong newGuid)
{
if (_player.CurrentAvatarGuid == newGuid)
return;
await DisappearAsync(_player.CurrentAvatarGuid, VisionType.VisionReplace);
_player.CurrentAvatarGuid = newGuid;
await SpawnActiveAvatarAsync(VisionType.VisionReplace);
}
private IEnumerable<GameReliquary> EquippedReliquaries(GameAvatar avatar)
=> avatar.ReliquaryGuids.Values.Select(_player.FindReliquary).OfType<GameReliquary>();
private async ValueTask DisappearAsync(ulong avatarGuid, VisionType visionType)
{
GameAvatar? avatar = _player.FindAvatar(avatarGuid);
if (avatar is null)
return;
await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, new SceneEntityDisappearNotify
{
DisappearType = visionType,
EntityList = { avatar.EntityId }
});
}
}