Gadgets are streamed alongside monsters now, so chests and ore nodes show up in the world and can be interacted with. The two flows differ: for a chest the client sends the finish request itself and the loot is granted on that one, for ore it sends nothing more so the server has to close everything on the first request. Artifacts used to be silently dropped on the floor because the item ids that actually roll were not in the reliquary table at all. Added the missing rows and gave them real main stat and substat rolls instead of the fixed four. Dropped items survive a relogin. The inventory is still rebuilt from the tables on every login, so the trick is to remember where the default set ends and only persist what was found in the world. Gacha and shop weapons were disappearing the same way and are fixed by the same change.
465 lines
18 KiB
C#
465 lines
18 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;
|
|
private const uint StarterReliquaryRank = 5;
|
|
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 List<GameGadget> Gadgets { get; } = new();
|
|
public HashSet<uint> KilledNodeIds { get; } = new();
|
|
public HashSet<uint> OpenedGadgetIds { 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 _defaultGuidCeiling;
|
|
private uint _avatarEntitySeq;
|
|
private uint _weaponEntitySeq;
|
|
private uint _monsterEntitySeq;
|
|
private uint _gadgetEntitySeq;
|
|
|
|
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);
|
|
_defaultGuidCeiling = _guidSeq;
|
|
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 GameGadget SpawnGadget(GadgetNode node)
|
|
{
|
|
GameGadget gadget = new(node, 0x04000000 + ++_gadgetEntitySeq);
|
|
Gadgets.Add(gadget);
|
|
return gadget;
|
|
}
|
|
|
|
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(),
|
|
OpenedGadgets = OpenedGadgetIds.ToList(),
|
|
DroppedWeapons = Weapons.Where(IsDropped).Select(weapon => new DroppedWeapon
|
|
{
|
|
ItemId = weapon.ItemId,
|
|
Level = weapon.Level,
|
|
PromoteLevel = weapon.PromoteLevel,
|
|
Refinement = weapon.Refinement
|
|
}).ToList(),
|
|
DroppedReliquaries = Reliquaries.Where(IsDropped).Select(reliquary => new DroppedReliquary
|
|
{
|
|
ItemId = reliquary.ItemId,
|
|
EquipType = reliquary.EquipType,
|
|
MainPropId = reliquary.MainPropId,
|
|
Level = reliquary.Level,
|
|
PromoteLevel = reliquary.PromoteLevel,
|
|
AppendPropIds = reliquary.AppendPropIds.ToList()
|
|
}).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;
|
|
|
|
RestoreDropped(record);
|
|
|
|
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);
|
|
|
|
OpenedGadgetIds.Clear();
|
|
foreach (uint gadgetNodeId in record.OpenedGadgets)
|
|
OpenedGadgetIds.Add(gadgetNodeId);
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
private bool IsDropped(GameWeapon weapon) => (uint)(weapon.Guid & 0xFFFFFFFF) > _defaultGuidCeiling;
|
|
|
|
private bool IsDropped(GameReliquary reliquary) => (uint)(reliquary.Guid & 0xFFFFFFFF) > _defaultGuidCeiling;
|
|
|
|
private void RestoreDropped(PlayerRecord record)
|
|
{
|
|
foreach (DroppedWeapon saved in record.DroppedWeapons)
|
|
{
|
|
GameWeapon weapon = CreateWeapon(saved.ItemId);
|
|
weapon.Level = saved.Level;
|
|
weapon.PromoteLevel = saved.PromoteLevel;
|
|
weapon.Refinement = saved.Refinement;
|
|
}
|
|
|
|
foreach (DroppedReliquary saved in record.DroppedReliquaries)
|
|
{
|
|
GameReliquary reliquary = new(saved.ItemId, NextGuid(), saved.EquipType, saved.MainPropId)
|
|
{
|
|
Level = saved.Level,
|
|
PromoteLevel = saved.PromoteLevel
|
|
};
|
|
|
|
foreach (uint appendPropId in saved.AppendPropIds)
|
|
reliquary.AppendPropIds.Add(appendPropId);
|
|
|
|
Reliquaries.Add(reliquary);
|
|
}
|
|
}
|
|
|
|
public GameWeapon CreateWeapon(uint itemId)
|
|
{
|
|
GameWeapon weapon = new(itemId, NextGuid());
|
|
Weapons.Add(weapon);
|
|
return weapon;
|
|
}
|
|
|
|
public GameReliquary? CreateReliquary(uint itemId, Random random)
|
|
{
|
|
ReliquaryExcel? excel = _excel.GetTable(ExcelType.Reliquary).Items.OfType<ReliquaryExcel>()
|
|
.FirstOrDefault(candidate => candidate.Id == itemId);
|
|
|
|
if (excel is null)
|
|
return null;
|
|
|
|
List<ReliquaryMainPropExcel> pool = _excel.GetTable(ExcelType.ReliquaryMainProp)
|
|
.Items.OfType<ReliquaryMainPropExcel>()
|
|
.Where(candidate => candidate.PropDepotId == excel.MainPropDepotId)
|
|
.ToList();
|
|
|
|
if (pool.Count == 0)
|
|
return null;
|
|
|
|
ReliquaryMainPropExcel main = pool[random.Next(pool.Count)];
|
|
GameReliquary reliquary = new(excel.Id, NextGuid(), EquipTypeId(excel.EquipType), main.Id)
|
|
{
|
|
Level = 1,
|
|
PromoteLevel = 0
|
|
};
|
|
|
|
List<IGrouping<uint, ReliquaryAffixExcel>> groups = _excel.GetTable(ExcelType.ReliquaryAffix)
|
|
.Items.OfType<ReliquaryAffixExcel>()
|
|
.Where(candidate => candidate.DepotId == excel.AppendPropDepotId && candidate.PropType != main.PropType)
|
|
.GroupBy(candidate => candidate.GroupId)
|
|
.ToList();
|
|
|
|
int wanted = (int)Math.Clamp(excel.AppendPropNum, 1u, (uint)groups.Count);
|
|
|
|
for (int taken = 0; taken < wanted && groups.Count != 0; taken++)
|
|
{
|
|
int index = random.Next(groups.Count);
|
|
List<ReliquaryAffixExcel> tiers = groups[index].ToList();
|
|
groups.RemoveAt(index);
|
|
|
|
reliquary.AppendPropIds.Add(tiers[random.Next(tiers.Count)].Id);
|
|
}
|
|
|
|
Reliquaries.Add(reliquary);
|
|
return reliquary;
|
|
}
|
|
|
|
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>()
|
|
.Where(candidate => candidate.RankLevel == StarterReliquaryRank))
|
|
{
|
|
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
|
|
};
|
|
}
|