diff --git a/FFXIVClassic Map Server/Database.cs b/FFXIVClassic Map Server/Database.cs index 995cdf20..643bda61 100644 --- a/FFXIVClassic Map Server/Database.cs +++ b/FFXIVClassic Map Server/Database.cs @@ -1218,7 +1218,7 @@ namespace FFXIVClassic_Map_Server } } - public static void EquipAbility(Player player, ushort hotbarSlot, uint commandId, uint recastTime) + public static void EquipAbility(Player player, byte classId, ushort hotbarSlot, uint commandId, uint recastTime) { //2700083201 is where abilities start. 2700083200 is for unequipping abilities. Trying to put this in the hotbar will crash the game, need to put 0 instead if (commandId > 2700083200) @@ -1245,7 +1245,7 @@ namespace FFXIVClassic_Map_Server cmd = new MySqlCommand(query, conn); cmd.Parameters.AddWithValue("@charId", player.actorId); - cmd.Parameters.AddWithValue("@classId", player.charaWork.parameterSave.state_mainSkill[0]); + cmd.Parameters.AddWithValue("@classId", classId); cmd.Parameters.AddWithValue("@commandId", commandId); cmd.Parameters.AddWithValue("@hotbarSlot", hotbarSlot); cmd.Parameters.AddWithValue("@recastTime", recastTime); @@ -1358,6 +1358,57 @@ namespace FFXIVClassic_Map_Server } } + public static ushort FindFirstCommandSlot(Player player, byte classId) + { + ushort slot = 0; + 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(); + MySqlCommand cmd; + string query = ""; + + //Drop + List> hotbarList = new List>(); + query = @" + SELECT hotbarSlot + FROM characters_hotbar + WHERE characterId = @charId AND classId = @classId + ORDER BY hotbarSlot + "; + cmd = new MySqlCommand(query, conn); + cmd.Parameters.AddWithValue("@charId", player.actorId); + cmd.Parameters.AddWithValue("@classId", classId); + + using (MySqlDataReader reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + if (slot != reader.GetUInt16("hotbarSlot")) + return slot; + + slot++; + } + } + } + catch (MySqlException e) + { + Program.Log.Error(e.ToString()); + } + finally + { + conn.Dispose(); + } + } + return slot; + } public static List GetInventory(Player player, uint slotOffset, uint type) { List items = new List(); @@ -2146,9 +2197,9 @@ namespace FFXIVClassic_Map_Server } } - public static Dictionary LoadGlobalBattleCommandList() + public static void LoadGlobalBattleCommandList(Dictionary battleCommandDict, Dictionary, uint> battleCommandIdByLevel) { - var battleCommands = new Dictionary(); + //var battleCommands = new Dictionary(); 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))) { @@ -2195,7 +2246,11 @@ namespace FFXIVClassic_Map_Server battleCommand.battleAnimation = reader.GetUInt32("battleAnimation"); battleCommand.validUser = (BattleCommandValidUser)reader.GetByte("validUser"); - battleCommands.Add(id, battleCommand); + battleCommandDict.Add(id, battleCommand); + + //Handle level 1 abilities separately because of FUCKING ARCHER REEE, just add those to the hotbar when the job is unlocked or on char creation, ez + if(battleCommand.level > 1) + battleCommandIdByLevel.Add(Tuple.Create(battleCommand.job, battleCommand.level), id | 0xA0F00000); } } } @@ -2208,7 +2263,6 @@ namespace FFXIVClassic_Map_Server conn.Dispose(); } } - return battleCommands; } } diff --git a/FFXIVClassic Map Server/WorldManager.cs b/FFXIVClassic Map Server/WorldManager.cs index 065955f4..47a61226 100644 --- a/FFXIVClassic Map Server/WorldManager.cs +++ b/FFXIVClassic Map Server/WorldManager.cs @@ -40,6 +40,7 @@ namespace FFXIVClassic_Map_Server private Dictionary currentPlayerParties = new Dictionary(); //GroupId, Party object private Dictionary statusEffectList = new Dictionary(); private Dictionary battleCommandList = new Dictionary(); + private Dictionary, uint> battleCommandIdByLevel = new Dictionary, uint>();//Holds battle command ids keyed by class id and level (in that order) private Dictionary battleNpcGenusMods = new Dictionary(); private Dictionary battleNpcPoolMods = new Dictionary(); private Dictionary battleNpcSpawnMods = new Dictionary(); @@ -1422,7 +1423,7 @@ namespace FFXIVClassic_Map_Server public void LoadBattleCommands() { - battleCommandList = Database.LoadGlobalBattleCommandList(); + Database.LoadGlobalBattleCommandList(battleCommandList, battleCommandIdByLevel); } public BattleCommand GetBattleCommand(uint id) @@ -1430,5 +1431,11 @@ namespace FFXIVClassic_Map_Server BattleCommand battleCommand; return battleCommandList.TryGetValue((ushort)id, out battleCommand) ? battleCommand.Clone() : null; } + + public uint GetBattleCommandIdByLevel(byte classId, short level) + { + uint id = 0; + return battleCommandIdByLevel.TryGetValue(Tuple.Create(classId, level), out id) ? id : 0; + } } } diff --git a/FFXIVClassic Map Server/actors/chara/BattleSave.cs b/FFXIVClassic Map Server/actors/chara/BattleSave.cs index bf9a6000..8d2928e4 100644 --- a/FFXIVClassic Map Server/actors/chara/BattleSave.cs +++ b/FFXIVClassic Map Server/actors/chara/BattleSave.cs @@ -5,7 +5,7 @@ public float potencial = 6.6f; public short[] skillLevel = new short[52]; public short[] skillLevelCap = new short[52]; - public short[] skillPoint = new short[52]; + public int[] skillPoint = new int[52]; public short physicalLevel; public int physicalExp; diff --git a/FFXIVClassic Map Server/actors/chara/CharaWork.cs b/FFXIVClassic Map Server/actors/chara/CharaWork.cs index 652178d6..45afc964 100644 --- a/FFXIVClassic Map Server/actors/chara/CharaWork.cs +++ b/FFXIVClassic Map Server/actors/chara/CharaWork.cs @@ -23,7 +23,7 @@ public uint[] command = new uint[64]; //ACTORS public byte[] commandCategory = new byte[64]; public byte commandBorder = 0x20; - public bool[] commandAcquired = new bool[4096]; + public bool[] commandAcquired = new bool[4096]; public bool[] additionalCommandAcquired = new bool[36]; public uint currentContentGroup; diff --git a/FFXIVClassic Map Server/actors/chara/player/Player.cs b/FFXIVClassic Map Server/actors/chara/player/Player.cs index fab60a05..2c139abd 100644 --- a/FFXIVClassic Map Server/actors/chara/player/Player.cs +++ b/FFXIVClassic Map Server/actors/chara/player/Player.cs @@ -956,11 +956,11 @@ namespace FFXIVClassic_Map_Server.Actors //Calculate hp/mp //Get Potenciel ?????? - + //Set HP/MP/TP PARAMS //Set mainskill and level - + //Set Parameters //Set current EXP @@ -972,7 +972,6 @@ namespace FFXIVClassic_Map_Server.Actors //Check if bonus point available... set //Set rested EXP - charaWork.parameterSave.state_mainSkill[0] = classId; charaWork.parameterSave.state_mainSkillLevel = charaWork.battleSave.skillLevel[classId-1]; playerWork.restBonusExpRate = 0.0f; @@ -1846,7 +1845,89 @@ namespace FFXIVClassic_Map_Server.Actors QueuePackets(recastPacketUtil.Done()); } - public void EquipAbility(ushort hotbarSlot, ushort commandId) + //Find the first open slot in classId's hotbar and equip an ability there. + public void EquipAbilityInFirstOpenSlot(byte classId, uint commandId, bool printMessage = true) + { + //Find first open slot on class's hotbar slot, then call EquipAbility with that slot. + ushort hotbarSlot = 0; + + //If the class we're equipping for is the current class, we can just look at charawork.command + if(classId == charaWork.parameterSave.state_mainSkill[0]) + { + hotbarSlot = FindFirstCommandSlotById(0); + } + //Otherwise, we need to check the database. + else + { + hotbarSlot = (ushort) (Database.FindFirstCommandSlot(this, classId) + charaWork.commandBorder); + } + + EquipAbility(classId, commandId, hotbarSlot, printMessage); + } + + //Add commandId to classId's hotbar at hotbarSlot. + //If classId is not the current class, do it in the database + //hotbarSlot is 32-indexed + public void EquipAbility(byte classId, uint commandId, ushort hotbarSlot, bool printMessage = true) + { + var ability = Server.GetWorldManager().GetBattleCommand(commandId); + uint trueCommandId = commandId | 0xA0F00000; + ushort lowHotbarSlot = (ushort)(hotbarSlot - charaWork.commandBorder); + ushort maxRecastTime = (ushort)ability.recastTimeSeconds; + uint recastEnd = Utils.UnixTimeStampUTC() + maxRecastTime; + List slotsToUpdate = new List(); + + //If the class we're equipping for is the current class (need to find out if state_mainSkill is supposed to change when you're a job) + //then equip the ability in charawork.commands and save in databse, otherwise just save in database + if (classId == charaWork.parameterSave.state_mainSkill[0]) + { + charaWork.command[hotbarSlot] = trueCommandId; + charaWork.commandCategory[hotbarSlot] = 1; + charaWork.parameterTemp.maxCommandRecastTime[lowHotbarSlot] = maxRecastTime; + charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot] = recastEnd; + + slotsToUpdate.Add(hotbarSlot); + UpdateHotbar(slotsToUpdate); + } + + Database.EquipAbility(this, classId, (ushort) (hotbarSlot - charaWork.commandBorder), commandId, recastEnd); + + if(printMessage) + SendGameMessage(Server.GetWorldManager().GetActor(), 30603, 0x20, 0, commandId); + } + + //Doesn't take a classId because the only way to swap abilities is through the ability equip widget oe /eaction, which only apply to current class + //hotbarSlot 1 and 2 are 32-indexed. + public void SwapAbilities(ushort hotbarSlot1, ushort hotbarSlot2) + { + uint lowHotbarSlot1 = (ushort)(hotbarSlot1 - charaWork.commandBorder); + uint lowHotbarSlot2 = (ushort)(hotbarSlot2 - charaWork.commandBorder); + + //Store information about first command + uint commandId = charaWork.command[hotbarSlot1]; + uint recastEnd = charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot1]; + ushort recastMax = charaWork.parameterTemp.maxCommandRecastTime[lowHotbarSlot1]; + + //Move second command's info to first hotbar slot + charaWork.command[hotbarSlot1] = charaWork.command[hotbarSlot2]; + charaWork.parameterTemp.maxCommandRecastTime[lowHotbarSlot1] = charaWork.parameterTemp.maxCommandRecastTime[lowHotbarSlot2]; + charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot1] = charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot2]; + + //Move first command's info to second slot + charaWork.command[hotbarSlot2] = commandId; + charaWork.parameterTemp.maxCommandRecastTime[lowHotbarSlot2] = recastMax; + charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot2] = recastEnd; + + //Save changes + Database.EquipAbility(this, charaWork.parameterSave.state_mainSkill[0], (ushort)(lowHotbarSlot1), charaWork.command[hotbarSlot1], charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot1]); + + List slotsToUpdate = new List(); + slotsToUpdate.Add(hotbarSlot1); + slotsToUpdate.Add(hotbarSlot2); + UpdateHotbar(slotsToUpdate); + } + + public void EquipAbility(ushort hotbarSlot, uint commandId) { var ability = Server.GetWorldManager().GetBattleCommand(commandId); uint trueCommandId = commandId | 0xA0F00000; @@ -1881,7 +1962,7 @@ namespace FFXIVClassic_Map_Server.Actors charaWork.parameterTemp.maxCommandRecastTime[oldSlot - charaWork.commandBorder] = charaWork.parameterTemp.maxCommandRecastTime[trueHotbarSlot - charaWork.commandBorder]; charaWork.parameterSave.commandSlot_recastTime[oldSlot - charaWork.commandBorder] = charaWork.parameterSave.commandSlot_recastTime[trueHotbarSlot - charaWork.commandBorder]; //Save changes - Database.EquipAbility(this, (ushort)(oldSlot - charaWork.commandBorder), charaWork.command[oldSlot], charaWork.parameterSave.commandSlot_recastTime[oldSlot - charaWork.commandBorder]); + Database.EquipAbility(this, charaWork.parameterSave.state_mainSkill[0], (ushort)(oldSlot - charaWork.commandBorder), charaWork.command[oldSlot], charaWork.parameterSave.commandSlot_recastTime[oldSlot - charaWork.commandBorder]); slotsToUpdate.Add(oldSlot); } @@ -1897,11 +1978,11 @@ namespace FFXIVClassic_Map_Server.Actors charaWork.parameterSave.commandSlot_recastTime[trueHotbarSlot - charaWork.commandBorder] = recastEnd; slotsToUpdate.Add(trueHotbarSlot); - Database.EquipAbility(this, (ushort) (trueHotbarSlot - charaWork.commandBorder), trueCommandId, recastEnd); + Database.EquipAbility(this, charaWork.parameterSave.state_mainSkill[0], (ushort) (trueHotbarSlot - charaWork.commandBorder), trueCommandId, recastEnd); //"[Command] set." if (!isAlreadyEquipped) - SendGameMessage(Server.GetWorldManager().GetActor(), 30603, 0x20, 0, commandId); + SendGameMessage(Server.GetWorldManager().GetActor(), 30603, 0x20, 0, commandId ^ 0xA0F00000); } //Ability is already equipped else if (isAlreadyEquipped) @@ -1920,7 +2001,7 @@ namespace FFXIVClassic_Map_Server.Actors } - public void UnequipAbility(ushort hotbarSlot) + public void UnequipAbility(ushort hotbarSlot, bool printMessage = true) { List slotsToUpdate = new List(); ushort trueHotbarSlot = (ushort)(hotbarSlot + charaWork.commandBorder - 1); @@ -1928,14 +2009,16 @@ namespace FFXIVClassic_Map_Server.Actors Database.UnequipAbility(this, (ushort)(trueHotbarSlot - charaWork.commandBorder)); charaWork.command[trueHotbarSlot] = 0; slotsToUpdate.Add(trueHotbarSlot); - SendGameMessage(Server.GetWorldManager().GetActor(), 30604, 0x20, 0, commandId ^ 0xA0F00000); + + if(printMessage) + SendGameMessage(Server.GetWorldManager().GetActor(), 30604, 0x20, 0, commandId ^ 0xA0F00000); UpdateHotbar(slotsToUpdate); } //Finds the first hotbar slot with a given commandId. //If the returned value is outside the hotbar, it indicates it wasn't found. - private ushort FindFirstCommandSlotById(uint commandId) + public ushort FindFirstCommandSlotById(uint commandId) { if(commandId != 0) commandId |= 0xA0F00000; @@ -2200,5 +2283,93 @@ namespace FFXIVClassic_Map_Server.Actors // todo: this really shouldnt be called on each ws? lua.LuaEngine.CallLuaBattleFunction(this, "onWeaponSkill", this, state.GetTarget(), skill); } + + //Handles exp being added, does not handle figuring out exp bonus from buffs or skill/link chains or any of that + public void AddExp(int exp, byte classId, int bonusPercent = 0) + { + exp += (int) Math.Ceiling((exp * bonusPercent / 100.0f)); + //You earn [exp](+[bonusPercent]%) experience point(s). + SendGameMessage(this, Server.GetWorldManager().GetActor(), 33934, 0x44, this, 0, 0, 0, 0, 0, 0, 0, 0, 0, exp, "", bonusPercent); + + bool leveled = false; + int diff = MAXEXP[GetLevel() - 1] - charaWork.battleSave.skillPoint[classId - 1]; + //While there is enough experience to level up, keep leveling up and unlocking skills and removing experience + while (exp >= diff && GetLevel() < charaWork.battleSave.skillLevelCap[classId]) + { + //Level up + LevelUp(classId); + leveled = true; + //Reduce exp based on how much exp is needed to level + exp -= diff; + diff = MAXEXP[GetLevel() - 1]; + } + + if(leveled) + { + //Set exp to current class to 0 so that exp is added correctly + charaWork.battleSave.skillPoint[classId - 1] = 0; + //send new level + ActorPropertyPacketUtil expPropertyPacket2 = new ActorPropertyPacketUtil("charaWork/exp", this); + ActorPropertyPacketUtil expPropertyPacket3 = new ActorPropertyPacketUtil("charaWork/stateForAll", this); + expPropertyPacket2.AddProperty($"charaWork.battleSave.skillLevel[{classId - 1}]"); + expPropertyPacket2.AddProperty($"charaWork.parameterSave.state_mainSkillLevel"); + QueuePackets(expPropertyPacket2.Done()); + QueuePackets(expPropertyPacket3.Done()); + //play levelup animation (do this outside LevelUp so that it only plays once if multiple levels are earned + //also i dunno how to do this + } + charaWork.battleSave.skillPoint[classId - 1] = Math.Min(charaWork.battleSave.skillPoint[classId - 1] + exp, MAXEXP[GetLevel() - 1]); + + ActorPropertyPacketUtil expPropertyPacket = new ActorPropertyPacketUtil("charaWork/battleStateForSelf", this); + expPropertyPacket.AddProperty($"charaWork.battleSave.skillPoint[{classId - 1}]"); + + //Cap experience for level 50 + QueuePackets(expPropertyPacket.Done()); + } + + public void LevelUp(byte classId) + { + if (charaWork.battleSave.skillLevel[classId - 1] < charaWork.battleSave.skillLevelCap[classId]) + { + //Increase level + charaWork.battleSave.skillLevel[classId - 1]++; + charaWork.parameterSave.state_mainSkillLevel++; + + SendGameMessage(this, Server.GetWorldManager().GetActor(), 33909, 0x44, this, 0, 0, 0, 0, 0, 0, 0, 0, 0, (int) GetLevel()); + //If there's an ability that unlocks at this level, equip it. + uint commandId = Server.GetWorldManager().GetBattleCommandIdByLevel(classId, GetLevel()); + if (commandId > 0) + { + EquipAbilityInFirstOpenSlot(classId, commandId, false); + byte jobId = ConvertClassIdToJobId(classId); + if (jobId != classId) + EquipAbilityInFirstOpenSlot(jobId, commandId, false); + } + } + } + + public static byte ConvertClassIdToJobId(byte classId) + { + byte jobId = classId; + + switch(classId) + { + case CLASSID_PUG: + case CLASSID_GLA: + case CLASSID_MRD: + jobId += 13; + break; + case CLASSID_ARC: + case CLASSID_LNC: + jobId += 10; + break; + case CLASSID_THM: + case CLASSID_CNJ: + jobId += 4; + break; + } + + return jobId; + } } } diff --git a/data/scripts/commands/EquipAbilityCommand.lua b/data/scripts/commands/EquipAbilityCommand.lua index e11e5d27..6b2251d8 100644 --- a/data/scripts/commands/EquipAbilityCommand.lua +++ b/data/scripts/commands/EquipAbilityCommand.lua @@ -10,8 +10,10 @@ function onEventStarted(player, equipAbilityWidget, triggername, slot, commandid local worldManager = GetWorldManager(); local ability = worldManager:GetBattleCommand(commandid); + --Equip if (commandid > 0) then + --[[]] --Can the player equip any more cross class actions if (player.charaWork.parameterTemp.otherClassAbilityCount[0] >= player.charaWork.parameterTemp.otherClassAbilityCount[1]) then --"You cannot set any more actions." @@ -19,30 +21,65 @@ function onEventStarted(player, equipAbilityWidget, triggername, slot, commandid player:endEvent(); return; end - + --Is the player high enough level in that class to equip the ability - if (player.charaWork.battleSave.skillLevel[ability.job] < ability.level) then - - --"You have not yet acquired that action" + if (player.charaWork.battleSave.skillLevel[ability.job - 1] < ability.level) then + --"You have not yet acquired that action." player:SendGameMessage(GetWorldMaster(), 30742, 0x20, 0, 0); player:endEvent(); return; end - --Equip the ability - player:EquipAbility(slot, commandid); + + local oldSlot = player:FindFirstCommandSlotById(commandid); + local isEquipped = oldSlot < player.charaWork.commandBorder + 30; + --If slot is 0, find the first open slot + if (slot == 0) then + --If the ability is already equipped and slot is 0, then it can't be equipped again + --If the slot isn't 0, it's a move or a swap command + if (isEquipped == true) then + --"That action is already set to an action slot." + player:SendGameMessage(GetWorldMaster(), 30719, 0x20, 0); + player:endEvent(); + return; + end + + slot = player:FindFirstCommandSlotById(0) - player.charaWork.commandBorder; + + --If the first open slot is outside the hotbar, then the hotbar is full + if(slot >= 30) then + --"You cannot set any more actions." + player:SendGameMessage(Server.GetWorldManager().GetActor(), 30720, 0x20, 0); + player:endEvent(); + return; + end + else + slot = slot - 1; + end + + if(isEquipped == true) then + player:SwapAbilities(oldSlot, slot + player.charaWork.commandBorder); + else + local tslot = slot + player.charaWork.commandBorder; + player:EquipAbility(player.GetJob(), commandid, tslot, true); + end + --Unequip elseif (commandid == 0) then - commandid = player.charaWork.command[slot + player.charaWork.commandBorder]; - + commandid = player.charaWork.command[slot + player.charaWork.commandBorder - 1]; + ability = worldManager.GetBattleCommand(commandid); --Is the ability a part of the player's current class? --This check isn't correct because of jobs having different ids - if(worldManager:GetBattleCommand(commandid).job == player.charaWork.parameterSave.state_mainSkill[0]) then - --"Actions of your current class or job cannot be removed." - player:SendGameMessage(GetWorldMaster(), 30745, 0x20, 0, 0); + local classId = player:GetJob(); + local jobId = player:ConvertClassIdToJobId(classId); + + if(ability.job == classId or ability.job == jobId) then + --"Actions of your current class or job cannot be removed." + player:SendGameMessage(GetWorldMaster(), 30745, 0x20, 0, 0); elseif (commandid != 0) then player:UnequipAbility(slot); end end + player:endEvent(); end \ No newline at end of file diff --git a/data/scripts/commands/gm/giveexp.lua b/data/scripts/commands/gm/giveexp.lua new file mode 100644 index 00000000..d27366ca --- /dev/null +++ b/data/scripts/commands/gm/giveexp.lua @@ -0,0 +1,34 @@ +require("global"); + +properties = { + permissions = 0, + parameters = "sss", + description = +[[ +Adds experience to player or . +!giveexp | +!giveexp | +]], +} + +function onTrigger(player, argc, qty, name, lastName) + local sender = "[giveexp] "; + + if name then + if lastName then + player = GetWorldManager():GetPCInWorld(name.." "..lastName) or nil; + else + player = GetWorldManager():GetPCInWorld(name) or nil; + end; + end; + + if player then + currency = 1000001; + qty = tonumber(qty) or 1; + location = INVENTORY_CURRENCY; + + player:AddExp(qty, player.charaWork.parameterSave.state_mainSkill[0], 5); + else + print(sender.."unable to add experience, ensure player name is valid."); + end; +end; \ No newline at end of file