Merge branch 'ai-open' into develop

# Conflicts:
#	FFXIVClassic Lobby Server/Database.cs
#	FFXIVClassic Map Server/Database.cs
#	FFXIVClassic Map Server/FFXIVClassic Map Server.csproj
#	FFXIVClassic Map Server/actors/chara/player/Inventory.cs
#	FFXIVClassic Map Server/actors/chara/player/Player.cs
#	FFXIVClassic Map Server/dataobjects/Session.cs
#	FFXIVClassic World Server/Server.cs
This commit is contained in:
Filip Maj 2019-05-04 20:13:29 -04:00
commit 1e4a1cf263
402 changed files with 20078 additions and 1348 deletions

View file

@ -22,6 +22,9 @@ using FFXIVClassic_Map_Server.packets.WorldPackets.Send.Group;
using System.Threading;
using System.Diagnostics;
using FFXIVClassic_Map_Server.actors.director;
using FFXIVClassic_Map_Server.actors.chara.ai;
using FFXIVClassic_Map_Server.actors.chara;
using FFXIVClassic_Map_Server.Actors.Chara;
namespace FFXIVClassic_Map_Server
{
@ -34,10 +37,18 @@ namespace FFXIVClassic_Map_Server
private Dictionary<uint, ZoneEntrance> zoneEntranceList;
private Dictionary<uint, ActorClass> actorClasses = new Dictionary<uint,ActorClass>();
private Dictionary<ulong, Party> currentPlayerParties = new Dictionary<ulong, Party>(); //GroupId, Party object
private Dictionary<uint, StatusEffect> statusEffectList = new Dictionary<uint, StatusEffect>();
private Dictionary<ushort, BattleCommand> battleCommandList = new Dictionary<ushort, BattleCommand>();
private Dictionary<Tuple<byte, short>, List<uint>> battleCommandIdByLevel = new Dictionary<Tuple<byte, short>, List<uint>>();//Holds battle command ids keyed by class id and level (in that order)
private Dictionary<ushort, BattleTrait> battleTraitList = new Dictionary<ushort, BattleTrait>();
private Dictionary<byte, List<ushort>> battleTraitIdsForClass = new Dictionary<byte, List<ushort>>();
private Dictionary<uint, ModifierList> battleNpcGenusMods = new Dictionary<uint, ModifierList>();
private Dictionary<uint, ModifierList> battleNpcPoolMods = new Dictionary<uint, ModifierList>();
private Dictionary<uint, ModifierList> battleNpcSpawnMods = new Dictionary<uint, ModifierList>();
private Server mServer;
private const int MILIS_LOOPTIME = 10;
private const int MILIS_LOOPTIME = 333;
private Timer mZoneTimer;
//Content Groups
@ -75,7 +86,8 @@ namespace FFXIVClassic_Map_Server
isInn,
canRideChocobo,
canStealth,
isInstanceRaid
isInstanceRaid,
loadNavMesh
FROM server_zones
WHERE zoneName IS NOT NULL and serverIp = @ip and serverPort = @port";
@ -88,7 +100,8 @@ namespace FFXIVClassic_Map_Server
{
while (reader.Read())
{
Zone zone = new Zone(reader.GetUInt32(0), reader.GetString(1), reader.GetUInt16(2), reader.GetString(3), reader.GetUInt16(4), reader.GetUInt16(5), reader.GetUInt16(6), reader.GetBoolean(7), reader.GetBoolean(8), reader.GetBoolean(9), reader.GetBoolean(10), reader.GetBoolean(11));
Zone zone = new Zone(reader.GetUInt32(0), reader.GetString(1), reader.GetUInt16(2), reader.GetString(3), reader.GetUInt16(4), reader.GetUInt16(5),
reader.GetUInt16(6), reader.GetBoolean(7), reader.GetBoolean(8), reader.GetBoolean(9), reader.GetBoolean(10), reader.GetBoolean(11), reader.GetBoolean(12));
zoneList[zone.actorId] = zone;
count1++;
}
@ -412,6 +425,121 @@ namespace FFXIVClassic_Map_Server
Program.Log.Info(String.Format("Loaded {0} spawn(s).", count));
}
public void LoadBattleNpcs()
{
LoadBattleNpcModifiers("server_battlenpc_genus_mods", "genusId", battleNpcGenusMods);
LoadBattleNpcModifiers("server_battlenpc_pool_mods", "poolId", battleNpcPoolMods);
LoadBattleNpcModifiers("server_battlenpc_spawn_mods", "bnpcId", battleNpcSpawnMods);
using (MySqlConnection conn = new MySqlConnection(String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}", ConfigConstants.DATABASE_HOST, ConfigConstants.DATABASE_PORT, ConfigConstants.DATABASE_NAME, ConfigConstants.DATABASE_USERNAME, ConfigConstants.DATABASE_PASSWORD)))
{
try
{
conn.Open();
var query = @"
SELECT bsl.bnpcId, bsl.groupId, bsl.positionX, bsl.positionY, bsl.positionZ, bsl.rotation,
bgr.groupId, bgr.poolId, bgr.scriptName, bgr.minLevel, bgr.maxLevel, bgr.respawnTime, bgr.hp, bgr.mp,
bgr.dropListId, bgr.allegiance, bgr.spawnType, bgr.animationId, bgr.actorState, bgr.privateAreaName, bgr.privateAreaLevel, bgr.zoneId,
bpo.poolId, bpo.genusId, bpo.actorClassId, bpo.currentJob, bpo.combatSkill, bpo.combatDelay, bpo.combatDmgMult, bpo.aggroType,
bpo.immunity, bpo.linkType, bpo.skillListId, bpo.spellListId,
bge.genusId, bge.modelSize, bge.speed, bge.kindredId, bge.detection, bge.hpp, bge.mpp, bge.tpp, bge.str, bge.vit, bge.dex,
bge.int, bge.mnd, bge.pie, bge.att, bge.acc, bge.def, bge.eva, bge.slash, bge.pierce, bge.h2h, bge.blunt,
bge.fire, bge.ice, bge.wind, bge.lightning, bge.earth, bge.water, bge.element
FROM server_battlenpc_spawn_locations bsl
INNER JOIN server_battlenpc_groups bgr ON bsl.groupId = bgr.groupId
INNER JOIN server_battlenpc_pools bpo ON bgr.poolId = bpo.poolId
INNER JOIN server_battlenpc_genus bge ON bpo.genusId = bge.genusId
WHERE bgr.zoneId = @zoneId GROUP BY bsl.bnpcId;
";
var count = 0;
foreach (var zonePair in zoneList)
{
Area zone = zonePair.Value;
MySqlCommand cmd = new MySqlCommand(query, conn);
cmd.Parameters.AddWithValue("@zoneId", zonePair.Key);
using (MySqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
int actorId = zone.GetActorCount() + 1;
// todo: add to private areas, set up immunity, mob linking,
// - load skill/spell/drop lists, set detection icon, load pool/family/group mods
var battleNpc = new BattleNpc(actorId, Server.GetWorldManager().GetActorClass(reader.GetUInt32("actorClassId")),
reader.GetString("scriptName"), zone, reader.GetFloat("positionX"), reader.GetFloat("positionY"), reader.GetFloat("positionZ"), reader.GetFloat("rotation"),
reader.GetUInt16("actorState"), reader.GetUInt32("animationId"), "");
battleNpc.SetBattleNpcId(reader.GetUInt32("bnpcId"));
battleNpc.poolId = reader.GetUInt32("poolId");
battleNpc.genusId = reader.GetUInt32("genusId");
battleNpcPoolMods.TryGetValue(battleNpc.poolId, out battleNpc.poolMods);
battleNpcGenusMods.TryGetValue(battleNpc.genusId, out battleNpc.genusMods);
battleNpcSpawnMods.TryGetValue(battleNpc.GetBattleNpcId(), out battleNpc.spawnMods);
battleNpc.SetMod((uint)Modifier.Speed, reader.GetByte("speed"));
battleNpc.neutral = reader.GetByte("aggroType") == 0;
battleNpc.SetDetectionType(reader.GetUInt32("detection"));
battleNpc.kindredType = (KindredType)reader.GetUInt32("kindredId");
battleNpc.npcSpawnType = (NpcSpawnType)reader.GetUInt32("spawnType");
battleNpc.charaWork.parameterSave.state_mainSkill[0] = reader.GetByte("currentJob");
battleNpc.charaWork.parameterSave.state_mainSkillLevel = (short)Program.Random.Next(reader.GetByte("minLevel"), reader.GetByte("maxLevel"));
battleNpc.allegiance = (CharacterTargetingAllegiance)reader.GetByte("allegiance");
// todo: setup private areas and other crap and
// set up rest of stat resists
battleNpc.SetMod((uint)Modifier.Hp, reader.GetUInt32("hp"));
battleNpc.SetMod((uint)Modifier.HpPercent, reader.GetUInt32("hpp"));
battleNpc.SetMod((uint)Modifier.Mp, reader.GetUInt32("mp"));
battleNpc.SetMod((uint)Modifier.MpPercent, reader.GetUInt32("mpp"));
battleNpc.SetMod((uint)Modifier.TpPercent, reader.GetUInt32("tpp"));
battleNpc.SetMod((uint)Modifier.Strength, reader.GetUInt32("str"));
battleNpc.SetMod((uint)Modifier.Vitality, reader.GetUInt32("vit"));
battleNpc.SetMod((uint)Modifier.Dexterity, reader.GetUInt32("dex"));
battleNpc.SetMod((uint)Modifier.Intelligence, reader.GetUInt32("int"));
battleNpc.SetMod((uint)Modifier.Mind, reader.GetUInt32("mnd"));
battleNpc.SetMod((uint)Modifier.Piety, reader.GetUInt32("pie"));
battleNpc.SetMod((uint)Modifier.Attack, reader.GetUInt32("att"));
battleNpc.SetMod((uint)Modifier.Accuracy, reader.GetUInt32("acc"));
battleNpc.SetMod((uint)Modifier.Defense, reader.GetUInt32("def"));
battleNpc.SetMod((uint)Modifier.Evasion, reader.GetUInt32("eva"));
battleNpc.dropListId = reader.GetUInt32("dropListId");
battleNpc.spellListId = reader.GetUInt32("spellListId");
battleNpc.skillListId = reader.GetUInt32("skillListId");
//battleNpc.SetMod((uint)Modifier.ResistFire, )
// todo: this is dumb
if (battleNpc.npcSpawnType == NpcSpawnType.Normal)
{
zone.AddActorToZone(battleNpc);
count++;
}
}
}
}
Program.Log.Info("Loaded {0} monsters.", count);
}
catch (MySqlException e)
{
Program.Log.Error(e.ToString());
}
finally
{
conn.Dispose();
}
}
}
public void SpawnAllActors()
{
Program.Log.Info("Spawning actors...");
@ -419,6 +547,200 @@ namespace FFXIVClassic_Map_Server
z.SpawnAllActors(true);
}
public BattleNpc SpawnBattleNpcById(uint id, Area area = null)
{
BattleNpc bnpc = null;
// todo: this is stupid duplicate code and really needs to die, think of a better way later
using (MySqlConnection conn = new MySqlConnection(String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}", ConfigConstants.DATABASE_HOST, ConfigConstants.DATABASE_PORT, ConfigConstants.DATABASE_NAME, ConfigConstants.DATABASE_USERNAME, ConfigConstants.DATABASE_PASSWORD)))
{
try
{
conn.Open();
var query = @"
SELECT bsl.bnpcId, bsl.groupId, bsl.positionX, bsl.positionY, bsl.positionZ, bsl.rotation,
bgr.groupId, bgr.poolId, bgr.scriptName, bgr.minLevel, bgr.maxLevel, bgr.respawnTime, bgr.hp, bgr.mp,
bgr.dropListId, bgr.allegiance, bgr.spawnType, bgr.animationId, bgr.actorState, bgr.privateAreaName, bgr.privateAreaLevel, bgr.zoneId,
bpo.poolId, bpo.genusId, bpo.actorClassId, bpo.currentJob, bpo.combatSkill, bpo.combatDelay, bpo.combatDmgMult, bpo.aggroType,
bpo.immunity, bpo.linkType, bpo.skillListId, bpo.spellListId,
bge.genusId, bge.modelSize, bge.speed, bge.kindredId, bge.detection, bge.hpp, bge.mpp, bge.tpp, bge.str, bge.vit, bge.dex,
bge.int, bge.mnd, bge.pie, bge.att, bge.acc, bge.def, bge.eva, bge.slash, bge.pierce, bge.h2h, bge.blunt,
bge.fire, bge.ice, bge.wind, bge.lightning, bge.earth, bge.water, bge.element
FROM server_battlenpc_spawn_locations bsl
INNER JOIN server_battlenpc_groups bgr ON bsl.groupId = bgr.groupId
INNER JOIN server_battlenpc_pools bpo ON bgr.poolId = bpo.poolId
INNER JOIN server_battlenpc_genus bge ON bpo.genusId = bge.genusId
WHERE bsl.bnpcId = @bnpcId GROUP BY bsl.bnpcId;
";
var count = 0;
MySqlCommand cmd = new MySqlCommand(query, conn);
cmd.Parameters.AddWithValue("@bnpcId", id);
using (MySqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
area = area ?? Server.GetWorldManager().GetZone(reader.GetUInt16("zoneId"));
int actorId = area.GetActorCount() + 1;
bnpc = area.GetBattleNpcById(id);
if (bnpc != null)
{
bnpc.ForceRespawn();
break;
}
// todo: add to private areas, set up immunity, mob linking,
// - load skill/spell/drop lists, set detection icon, load pool/family/group mods
var allegiance = (CharacterTargetingAllegiance)reader.GetByte("allegiance");
BattleNpc battleNpc = null;
if (allegiance == CharacterTargetingAllegiance.Player)
battleNpc = new Ally(actorId, Server.GetWorldManager().GetActorClass(reader.GetUInt32("actorClassId")),
reader.GetString("scriptName"), area, reader.GetFloat("positionX"), reader.GetFloat("positionY"), reader.GetFloat("positionZ"), reader.GetFloat("rotation"),
reader.GetUInt16("actorState"), reader.GetUInt32("animationId"), "");
else
battleNpc = new BattleNpc(actorId, Server.GetWorldManager().GetActorClass(reader.GetUInt32("actorClassId")),
reader.GetString("scriptName"), area, reader.GetFloat("positionX"), reader.GetFloat("positionY"), reader.GetFloat("positionZ"), reader.GetFloat("rotation"),
reader.GetUInt16("actorState"), reader.GetUInt32("animationId"), "");
battleNpc.SetBattleNpcId(reader.GetUInt32("bnpcId"));
battleNpc.SetMod((uint)Modifier.Speed, reader.GetByte("speed"));
battleNpc.neutral = reader.GetByte("aggroType") == 0;
// set mob mods
battleNpc.poolId = reader.GetUInt32("poolId");
battleNpc.genusId = reader.GetUInt32("genusId");
battleNpcPoolMods.TryGetValue(battleNpc.poolId, out battleNpc.poolMods);
battleNpcGenusMods.TryGetValue(battleNpc.genusId, out battleNpc.genusMods);
battleNpcSpawnMods.TryGetValue(battleNpc.GetBattleNpcId(), out battleNpc.spawnMods);
battleNpc.SetDetectionType(reader.GetUInt32("detection"));
battleNpc.kindredType = (KindredType)reader.GetUInt32("kindredId");
battleNpc.npcSpawnType = (NpcSpawnType)reader.GetUInt32("spawnType");
battleNpc.charaWork.parameterSave.state_mainSkill[0] = reader.GetByte("currentJob");
battleNpc.charaWork.parameterSave.state_mainSkillLevel = (short)Program.Random.Next(reader.GetByte("minLevel"), reader.GetByte("maxLevel"));
battleNpc.allegiance = (CharacterTargetingAllegiance)reader.GetByte("allegiance");
// todo: setup private areas and other crap and
// set up rest of stat resists
battleNpc.SetMod((uint)Modifier.Hp, reader.GetUInt32("hp"));
battleNpc.SetMod((uint)Modifier.HpPercent, reader.GetUInt32("hpp"));
battleNpc.SetMod((uint)Modifier.Mp, reader.GetUInt32("mp"));
battleNpc.SetMod((uint)Modifier.MpPercent, reader.GetUInt32("mpp"));
battleNpc.SetMod((uint)Modifier.TpPercent, reader.GetUInt32("tpp"));
battleNpc.SetMod((uint)Modifier.Strength, reader.GetUInt32("str"));
battleNpc.SetMod((uint)Modifier.Vitality, reader.GetUInt32("vit"));
battleNpc.SetMod((uint)Modifier.Dexterity, reader.GetUInt32("dex"));
battleNpc.SetMod((uint)Modifier.Intelligence, reader.GetUInt32("int"));
battleNpc.SetMod((uint)Modifier.Mind, reader.GetUInt32("mnd"));
battleNpc.SetMod((uint)Modifier.Piety, reader.GetUInt32("pie"));
battleNpc.SetMod((uint)Modifier.Attack, reader.GetUInt32("att"));
battleNpc.SetMod((uint)Modifier.Accuracy, reader.GetUInt32("acc"));
battleNpc.SetMod((uint)Modifier.Defense, reader.GetUInt32("def"));
battleNpc.SetMod((uint)Modifier.Evasion, reader.GetUInt32("eva"));
if (battleNpc.poolMods != null)
{
foreach (var a in battleNpc.poolMods.mobModList)
{
battleNpc.SetMobMod(a.Value.id, (long)(a.Value.value));
}
foreach (var a in battleNpc.poolMods.modList)
{
battleNpc.SetMod(a.Key, (long)(a.Value.value));
}
}
if (battleNpc.genusMods != null)
{
foreach (var a in battleNpc.genusMods.mobModList)
{
battleNpc.SetMobMod(a.Key, (long)(a.Value.value));
}
foreach (var a in battleNpc.genusMods.modList)
{
battleNpc.SetMod(a.Key, (long)(a.Value.value));
}
}
if(battleNpc.spawnMods != null)
{
foreach (var a in battleNpc.spawnMods.mobModList)
{
battleNpc.SetMobMod(a.Key, (long)(a.Value.value));
}
foreach (var a in battleNpc.spawnMods.modList)
{
battleNpc.SetMod(a.Key, (long)(a.Value.value));
}
}
battleNpc.dropListId = reader.GetUInt32("dropListId");
battleNpc.spellListId = reader.GetUInt32("spellListId");
battleNpc.skillListId = reader.GetUInt32("skillListId");
battleNpc.SetBattleNpcId(reader.GetUInt32("bnpcId"));
battleNpc.SetRespawnTime(reader.GetUInt32("respawnTime"));
battleNpc.CalculateBaseStats();
battleNpc.RecalculateStats();
//battleNpc.SetMod((uint)Modifier.ResistFire, )
bnpc = battleNpc;
area.AddActorToZone(battleNpc);
count++;
}
}
Program.Log.Info("WorldManager.SpawnBattleNpcById spawned BattleNpc {0}.", id);
}
catch (MySqlException e)
{
Program.Log.Error(e.ToString());
}
finally
{
conn.Dispose();
}
}
return bnpc;
}
public void LoadBattleNpcModifiers(string tableName, string primaryKey, Dictionary<uint, ModifierList> list)
{
using (MySqlConnection conn = new MySqlConnection(String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}", ConfigConstants.DATABASE_HOST, ConfigConstants.DATABASE_PORT, ConfigConstants.DATABASE_NAME, ConfigConstants.DATABASE_USERNAME, ConfigConstants.DATABASE_PASSWORD)))
{
try
{
conn.Open();
var query = $"SELECT {primaryKey}, modId, modVal, isMobMod FROM {tableName}";
MySqlCommand cmd = new MySqlCommand(query, conn);
using (MySqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetUInt32(primaryKey);
ModifierList modList = list.TryGetValue(id, out modList) ? modList : new ModifierList(id);
modList.SetModifier(reader.GetUInt16("modId"), reader.GetInt64("modVal"), reader.GetBoolean("isMobMod"));
list[id] = modList;
}
}
}
catch (MySqlException e)
{
Program.Log.Error(e.ToString());
}
finally
{
conn.Dispose();
}
}
}
//Moves the actor to the new zone if exists. No packets are sent nor position changed. Merged zone is removed.
public void DoSeamlessZoneChange(Player player, uint destinationZoneId)
{
@ -581,7 +903,6 @@ namespace FFXIVClassic_Map_Server
{
oldZone.RemoveActorFromZone(player);
}
newArea.AddActorToZone(player);
//Update player actor's properties
@ -757,13 +1078,15 @@ namespace FFXIVClassic_Map_Server
public void ReloadZone(uint zoneId)
{
if (!zoneList.ContainsKey(zoneId))
return;
Zone zone = zoneList[zoneId];
//zone.clear();
//LoadNPCs(zone.actorId);
lock (zoneList)
{
if (!zoneList.ContainsKey(zoneId))
return;
Zone zone = zoneList[zoneId];
//zone.clear();
//LoadNPCs(zone.actorId);
}
}
public ContentGroup CreateContentGroup(Director director, params Actor[] actors)
@ -1005,11 +1328,17 @@ namespace FFXIVClassic_Map_Server
}
public void ZoneThreadLoop(Object state)
{
{
// todo: coroutines GetActorInWorld stuff seems to be causing it to hang
// todo: spawn new thread for each zone on startup
lock (zoneList)
{
foreach (Area area in zoneList.Values)
area.Update(MILIS_LOOPTIME);
Program.Tick = DateTime.Now;
foreach (Zone zone in zoneList.Values)
{
zone.Update(Program.Tick);
}
Program.LastTick = Program.Tick;
}
}
@ -1031,39 +1360,52 @@ namespace FFXIVClassic_Map_Server
public Actor GetActorInWorld(uint charId)
{
foreach (Zone zone in zoneList.Values)
lock (zoneList)
{
Actor a = zone.FindActorInZone(charId);
if (a != null)
return a;
foreach (Zone zone in zoneList.Values)
{
Actor a = zone.FindActorInZone(charId);
if (a != null)
return a;
}
}
return null;
}
public Actor GetActorInWorldByUniqueId(string uid)
{
foreach (Zone zone in zoneList.Values)
lock (zoneList)
{
Actor a = zone.FindActorInZoneByUniqueID(uid);
if (a != null)
return a;
foreach (Zone zone in zoneList.Values)
{
Actor a = zone.FindActorInZoneByUniqueID(uid);
if (a != null)
return a;
}
}
return null;
}
public Zone GetZone(uint zoneId)
{
if (!zoneList.ContainsKey(zoneId))
return null;
return zoneList[zoneId];
lock (zoneList)
{
if (!zoneList.ContainsKey(zoneId))
return null;
return zoneList[zoneId];
}
}
public PrivateArea GetPrivateArea(uint zoneId, string privateArea, uint privateAreaType)
{
if (!zoneList.ContainsKey(zoneId))
return null;
lock (zoneList)
{
if (!zoneList.ContainsKey(zoneId))
return null;
return zoneList[zoneId].GetPrivateArea(privateArea, privateAreaType);
return zoneList[zoneId].GetPrivateArea(privateArea, privateAreaType);
}
}
public WorldMaster GetActor()
@ -1115,5 +1457,52 @@ namespace FFXIVClassic_Map_Server
else
return null;
}
public void LoadStatusEffects()
{
statusEffectList = Database.LoadGlobalStatusEffectList();
}
public StatusEffect GetStatusEffect(uint id)
{
StatusEffect statusEffect;
return statusEffectList.TryGetValue(id, out statusEffect) ? new StatusEffect(null, statusEffect) : null;
}
public void LoadBattleCommands()
{
Database.LoadGlobalBattleCommandList(battleCommandList, battleCommandIdByLevel);
}
public void LoadBattleTraits()
{
Database.LoadGlobalBattleTraitList(battleTraitList, battleTraitIdsForClass);
}
public BattleCommand GetBattleCommand(uint id)
{
BattleCommand battleCommand;
return battleCommandList.TryGetValue((ushort)id, out battleCommand) ? battleCommand.Clone() : null;
}
public List<uint> GetBattleCommandIdByLevel(byte classId, short level)
{
List<uint> ids;
return battleCommandIdByLevel.TryGetValue(Tuple.Create(classId, level), out ids) ? ids : new List<uint>();
}
public BattleTrait GetBattleTrait(ushort id)
{
BattleTrait battleTrait;
battleTraitList.TryGetValue(id, out battleTrait);
return battleTrait;
}
public List<ushort> GetAllBattleTraitIdsForClass(byte classId)
{
List<ushort> ids;
return battleTraitIdsForClass.TryGetValue(classId, out ids) ? ids : new List<ushort>();
}
}
}