yuanshen/Yuanshen.Gameserver/Game/Player.cs
wisedev 61cc2055cf 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:06:53 +03:00

355 lines
14 KiB
C#

using Yuanshen.Common.Data.Excel;
using Yuanshen.Gameserver.Database;
namespace Yuanshen.Gameserver.Game;
internal sealed class Player
{
public uint Uid { get; private set; } = 1;
public const uint PeerId = 1;
public const uint TeamEntityId = 0x09000001;
public const uint MpLevelEntityId = 0x0B000001;
public static readonly uint[] Flycloaks = { 140001, 140002, 140003, 140004, 140005 };
public string Nickname { get; } = "Traveler";
public List<GameAvatar> Avatars { get; } = new();
public List<GameWeapon> Weapons { get; } = new();
public List<GameReliquary> Reliquaries { get; } = new();
public List<GameMonster> Monsters { get; } = new();
public HashSet<uint> KilledNodeIds { get; } = new();
public Dictionary<uint, uint> Materials { get; } = new();
public bool IsInitialized { get; private set; }
public uint CurrentTeamId { get; set; } = 1;
public List<ulong> TeamAvatarGuids { get; } = new();
public ulong CurrentAvatarGuid { get; set; }
private readonly ExcelTableCollection _excel;
private uint _guidSeq;
private uint _avatarEntitySeq;
private uint _weaponEntitySeq;
private uint _monsterEntitySeq;
public Player(ExcelTableCollection excel) => _excel = excel;
public void AssignUid(uint uid) => Uid = uid;
public const uint StartingHcoin = 1_600_000;
public const uint StartingScoin = 9_999_999;
public const uint CurrentSpawnDataVersion = 2;
public ulong NextGuid() => ((ulong)Uid << 32) | ++_guidSeq;
public GameAvatar? ActiveAvatar => FindAvatar(CurrentAvatarGuid);
public GameAvatar? FindAvatar(ulong guid) => Avatars.FirstOrDefault(avatar => avatar.Guid == guid);
public GameWeapon? FindWeapon(ulong guid) => Weapons.FirstOrDefault(weapon => weapon.Guid == guid);
public GameReliquary? FindReliquary(ulong guid) => Reliquaries.FirstOrDefault(item => item.Guid == guid);
public GameReliquary? FindReliquaryByItemId(uint itemId) => Reliquaries.FirstOrDefault(item => item.ItemId == itemId);
public IEnumerable<GameAvatar> TeamAvatars
=> TeamAvatarGuids.Select(FindAvatar).OfType<GameAvatar>();
public void InitDefault()
{
if (IsInitialized)
return;
List<AvatarSkillDepotExcel> depots = _excel.GetTable(ExcelType.AvatarSkillDepot)
.Items.OfType<AvatarSkillDepotExcel>().ToList();
List<AvatarAbilitiesExcel> abilities = _excel.GetTable(ExcelType.AvatarAbilities)
.Items.OfType<AvatarAbilitiesExcel>().ToList();
foreach (AvatarExcel excel in _excel.GetTable(ExcelType.Avatar).Items.OfType<AvatarExcel>())
{
AvatarAbilitiesExcel? avatarAbilities = abilities.FirstOrDefault(a => a.Id == excel.Id);
if (avatarAbilities is null)
continue;
AvatarSkillDepotExcel? depot = depots.FirstOrDefault(d => d.Id == excel.SkillDepotId);
GameAvatar avatar = new(excel, NextGuid(), 0x01000000 + ++_avatarEntitySeq, 0x06000000 + ++_weaponEntitySeq, depot,
avatarAbilities);
GameWeapon weapon = CreateWeapon(excel.InitialWeapon);
avatar.WeaponGuid = weapon.Guid;
avatar.OwnerUid = Uid;
Avatars.Add(avatar);
}
foreach (WeaponExcel excel in _excel.GetTable(ExcelType.Weapon).Items.OfType<WeaponExcel>())
CreateWeapon(excel.Id);
CreateReliquaries();
CurrentAvatarGuid = Avatars[0].Guid;
TeamAvatarGuids.Add(Avatars[0].Guid);
IsInitialized = true;
}
public GameAvatar? GrantAvatar(uint avatarId)
{
if (Avatars.Any(existing => existing.AvatarId == avatarId))
return null;
AvatarExcel? excel = _excel.GetTable(ExcelType.Avatar).Items.OfType<AvatarExcel>()
.FirstOrDefault(candidate => candidate.Id == avatarId);
AvatarAbilitiesExcel? abilities = _excel.GetTable(ExcelType.AvatarAbilities).Items.OfType<AvatarAbilitiesExcel>()
.FirstOrDefault(candidate => candidate.Id == avatarId);
if (excel is null)
return null;
AvatarSkillDepotExcel? depot = _excel.GetTable(ExcelType.AvatarSkillDepot).Items.OfType<AvatarSkillDepotExcel>()
.FirstOrDefault(candidate => candidate.Id == excel.SkillDepotId);
GameAvatar avatar = new(excel, NextGuid(), 0x01000000 + ++_avatarEntitySeq, 0x06000000 + ++_weaponEntitySeq, depot, abilities);
GameWeapon weapon = CreateWeapon(excel.InitialWeapon);
avatar.WeaponGuid = weapon.Guid;
avatar.OwnerUid = Uid;
Avatars.Add(avatar);
return avatar;
}
public bool HasAvatarData(uint avatarId)
=> _excel.GetTable(ExcelType.Avatar).Items.OfType<AvatarExcel>().Any(candidate => candidate.Id == avatarId);
public bool HasAbilities(uint avatarId)
=> _excel.GetTable(ExcelType.AvatarAbilities).Items.OfType<AvatarAbilitiesExcel>().Any(candidate => candidate.Id == avatarId);
public void ResetTeam()
{
TeamAvatarGuids.Clear();
TeamAvatarGuids.Add(Avatars[0].Guid);
CurrentAvatarGuid = Avatars[0].Guid;
}
public ulong MaterialGuid(uint itemId) => ((ulong)Uid << 32) | (0x80000000u | itemId);
public void MarkNodeKilled(uint nodeId) => KilledNodeIds.Add(nodeId);
public Dictionary<uint, uint> GachaPity { get; } = new();
public Dictionary<uint, uint> GachaGuarantee { get; } = new();
public List<GachaLogEntry> GachaLog { get; } = new();
public Dictionary<uint, uint> BoughtGoods { get; } = new();
public Dictionary<uint, uint> ClientGadgets { get; } = new();
public uint Hcoin { get; set; } = StartingHcoin;
public uint Scoin { get; set; } = StartingScoin;
public Yuanshen.Protocol.PlayerPropNotify BuildCurrencyProps()
{
Yuanshen.Protocol.PlayerPropNotify notify = new();
notify.PropMap.Add(10015u, new Yuanshen.Protocol.PropValue { Type = 10015, Ival = Hcoin, Val = Hcoin });
notify.PropMap.Add(10016u, new Yuanshen.Protocol.PropValue { Type = 10016, Ival = Scoin, Val = Scoin });
return notify;
}
public void TakeMaterial(uint itemId, uint count)
{
uint left = Materials.GetValueOrDefault(itemId);
Materials[itemId] = left > count ? left - count : 0;
}
public void GiveMaterial(uint itemId, uint count)
=> Materials[itemId] = Materials.GetValueOrDefault(itemId) + count;
public GameMonster SpawnMonster(uint monsterId, Yuanshen.Protocol.Vector pos, uint level, float maxHp, float attack, float defense, uint nodeId = 0, uint configId = 0, uint groupId = 0)
{
GameMonster monster = new(monsterId, 0x02000000 + ++_monsterEntitySeq, pos, level, maxHp, attack, defense)
{
NodeId = nodeId,
ConfigId = configId,
GroupId = groupId
};
Monsters.Add(monster);
return monster;
}
public PlayerRecord ToRecord(Yuanshen.Protocol.Vector pos) => new()
{
Uid = Uid,
Nickname = Nickname,
CurrentTeamId = CurrentTeamId,
CurrentAvatarGuid = CurrentAvatarGuid,
TeamAvatarGuids = TeamAvatarGuids.ToList(),
TeamAvatarIds = TeamAvatars.Select(avatar => avatar.AvatarId).ToList(),
CurrentAvatarId = ActiveAvatar?.AvatarId ?? 0,
EquippedWeapons = Avatars
.GroupBy(a => a.AvatarId)
.ToDictionary(group => group.Key, group => FindWeapon(group.First().WeaponGuid)?.ItemId ?? 0UL),
EquippedReliquaries = Avatars
.Where(a => a.ReliquaryGuids.Count != 0)
.GroupBy(a => a.AvatarId)
.ToDictionary(group => group.Key, group => group.First().ReliquaryGuids
.ToDictionary(slot => slot.Key, slot => (ulong)(FindReliquary(slot.Value)?.ItemId ?? 0))),
AvatarProgress = Avatars
.GroupBy(a => a.AvatarId)
.ToDictionary(group => group.Key, group => BuildProgress(group.First())),
Materials = new Dictionary<uint, uint>(Materials),
KilledNodes = KilledNodeIds.ToList(),
SpawnDataVersion = CurrentSpawnDataVersion,
GachaPity = GachaPity.ToDictionary(pair => pair.Key, pair => pair.Value),
GachaGuarantee = GachaGuarantee.ToDictionary(pair => pair.Key, pair => pair.Value),
GachaLog = GachaLog.ToList(),
BoughtGoods = BoughtGoods.ToDictionary(pair => pair.Key, pair => pair.Value),
Hcoin = Hcoin,
Scoin = Scoin,
PosX = pos.X,
PosY = pos.Y,
PosZ = pos.Z
};
public void ApplyRecord(PlayerRecord record)
{
CurrentTeamId = record.CurrentTeamId;
Hcoin = record.Hcoin != 0 || record.Scoin != 0 ? record.Hcoin : StartingHcoin;
Scoin = record.Hcoin != 0 || record.Scoin != 0 ? record.Scoin : StartingScoin;
BoughtGoods.Clear();
foreach ((uint goodsId, uint bought) in record.BoughtGoods)
BoughtGoods[goodsId] = bought;
GachaPity.Clear();
foreach ((uint gachaType, uint counter) in record.GachaPity)
GachaPity[gachaType] = counter;
GachaGuarantee.Clear();
foreach ((uint gachaType, uint flag) in record.GachaGuarantee)
GachaGuarantee[gachaType] = flag;
GachaLog.Clear();
GachaLog.AddRange(record.GachaLog);
KilledNodeIds.Clear();
foreach (uint nodeId in record.SpawnDataVersion == CurrentSpawnDataVersion ? record.KilledNodes : new List<uint>())
KilledNodeIds.Add(nodeId);
HashSet<ulong> takenWeapons = new();
foreach ((uint avatarId, ulong itemId) in record.EquippedWeapons)
{
GameAvatar? avatar = Avatars.FirstOrDefault(a => a.AvatarId == avatarId);
GameWeapon? weapon = Weapons.FirstOrDefault(w => w.ItemId == itemId && !takenWeapons.Contains(w.Guid));
if (avatar is null || weapon is null)
continue;
avatar.WeaponGuid = weapon.Guid;
takenWeapons.Add(weapon.Guid);
}
HashSet<ulong> takenReliquaries = new();
foreach ((uint avatarId, Dictionary<uint, ulong> slots) in record.EquippedReliquaries)
{
GameAvatar? avatar = Avatars.FirstOrDefault(a => a.AvatarId == avatarId);
if (avatar is null)
continue;
foreach ((uint slot, ulong itemId) in slots)
{
GameReliquary? reliquary = Reliquaries
.FirstOrDefault(r => r.ItemId == itemId && !takenReliquaries.Contains(r.Guid));
if (reliquary is null)
continue;
avatar.ReliquaryGuids[slot] = reliquary.Guid;
takenReliquaries.Add(reliquary.Guid);
}
}
foreach ((uint avatarId, AvatarProgress progress) in record.AvatarProgress)
{
GameAvatar? avatar = Avatars.FirstOrDefault(a => a.AvatarId == avatarId);
if (avatar is null)
continue;
avatar.Level = progress.Level;
avatar.PromoteLevel = progress.PromoteLevel;
avatar.TalentLevel = progress.TalentLevel;
avatar.Constellation = progress.Constellation;
GameWeapon? weapon = FindWeapon(avatar.WeaponGuid);
if (weapon is not null)
{
weapon.Level = progress.WeaponLevel;
weapon.PromoteLevel = progress.WeaponPromote;
weapon.Refinement = progress.WeaponRefinement;
}
}
Materials.Clear();
foreach ((uint itemId, uint count) in record.Materials)
Materials[itemId] = count;
List<ulong> team = record.TeamAvatarIds
.Select(avatarId => Avatars.FirstOrDefault(a => a.AvatarId == avatarId)?.Guid ?? 0)
.Where(guid => guid != 0)
.ToList();
if (team.Count != 0)
{
TeamAvatarGuids.Clear();
TeamAvatarGuids.AddRange(team);
ulong active = Avatars.FirstOrDefault(a => a.AvatarId == record.CurrentAvatarId)?.Guid ?? 0;
CurrentAvatarGuid = team.Contains(active) ? active : team[0];
}
}
private AvatarProgress BuildProgress(GameAvatar avatar)
{
GameWeapon? weapon = FindWeapon(avatar.WeaponGuid);
return new AvatarProgress
{
Level = avatar.Level,
PromoteLevel = avatar.PromoteLevel,
TalentLevel = avatar.TalentLevel,
Constellation = avatar.Constellation,
WeaponLevel = weapon?.Level ?? 90,
WeaponPromote = weapon?.PromoteLevel ?? 6,
WeaponRefinement = weapon?.Refinement ?? 0
};
}
public GameWeapon CreateWeapon(uint itemId)
{
GameWeapon weapon = new(itemId, NextGuid());
Weapons.Add(weapon);
return weapon;
}
private void CreateReliquaries()
{
List<ReliquaryMainPropExcel> mainProps = _excel.GetTable(ExcelType.ReliquaryMainProp)
.Items.OfType<ReliquaryMainPropExcel>().ToList();
List<ReliquaryAffixExcel> affixes = _excel.GetTable(ExcelType.ReliquaryAffix)
.Items.OfType<ReliquaryAffixExcel>().ToList();
foreach (ReliquaryExcel excel in _excel.GetTable(ExcelType.Reliquary).Items.OfType<ReliquaryExcel>())
{
ReliquaryMainPropExcel? main = mainProps.FirstOrDefault(p => p.PropDepotId == excel.MainPropDepotId);
if (main is null)
continue;
GameReliquary reliquary = new(excel.Id, NextGuid(), EquipTypeId(excel.EquipType), main.Id);
foreach (ReliquaryAffixExcel affix in affixes.Where(a => a.DepotId == excel.AppendPropDepotId).Take(4))
reliquary.AppendPropIds.Add(affix.Id);
Reliquaries.Add(reliquary);
}
}
public static uint EquipTypeId(string equipType) => equipType switch
{
"EQUIP_BRACER" => 1,
"EQUIP_NECKLACE" => 2,
"EQUIP_SHOES" => 3,
"EQUIP_RING" => 4,
"EQUIP_DRESS" => 5,
_ => 0
};
}