From a7f0e29d37d6683b6a4c499e3e3b18bd1bda545d Mon Sep 17 00:00:00 2001 From: Zach Toogood Date: Sat, 2 May 2026 16:50:39 +0100 Subject: [PATCH 1/2] Core: make chocoboRaisingInfo upserts --- sql/char_chocobos.sql | 5 +- src/map/lua/lua_baseentity.cpp | 53 ++++++++++++++++----- tools/migrations/051_add_chocobo_alleles.py | 27 +++++++++++ 3 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 tools/migrations/051_add_chocobo_alleles.py diff --git a/sql/char_chocobos.sql b/sql/char_chocobos.sql index 0ed86ca379e..02278540cec 100644 --- a/sql/char_chocobos.sql +++ b/sql/char_chocobos.sql @@ -16,8 +16,9 @@ CREATE TABLE `char_chocobos` ( `stage` tinyint unsigned NOT NULL, `location` tinyint unsigned NOT NULL, `color` tinyint unsigned NOT NULL, - `dominant_gene` tinyint unsigned NOT NULL, - `recessive_gene` tinyint unsigned NOT NULL, + `allele1` tinyint unsigned NOT NULL DEFAULT 0, + `allele2` tinyint unsigned NOT NULL DEFAULT 0, + `allele3` tinyint unsigned NOT NULL DEFAULT 0, `strength` tinyint unsigned NOT NULL, `endurance` tinyint unsigned NOT NULL, `discernment` tinyint unsigned NOT NULL, diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 1c4ea017486..853279e3a12 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -19199,8 +19199,9 @@ auto CLuaBaseEntity::getChocoboRaisingInfo() -> sol::table "stage, " "location, " "color, " - "dominant_gene, " - "recessive_gene, " + "allele1, " + "allele2, " + "allele3, " "strength, " "endurance, " "discernment, " @@ -19245,8 +19246,9 @@ auto CLuaBaseEntity::getChocoboRaisingInfo() -> sol::table table["location"] = rset->get("location"); table["color"] = rset->get("color"); - table["dominant_gene"] = rset->get("dominant_gene"); - table["recessive_gene"] = rset->get("recessive_gene"); + table["allele1"] = rset->get("allele1"); + table["allele2"] = rset->get("allele2"); + table["allele3"] = rset->get("allele3"); table["strength"] = rset->get("strength"); table["endurance"] = rset->get("endurance"); @@ -19283,7 +19285,7 @@ bool CLuaBaseEntity::setChocoboRaisingInfo(const sol::table& table) return false; } - const char* Query = "REPLACE INTO char_chocobos SET " + const char* Query = "INSERT INTO char_chocobos SET " "charid = ?, " "first_name = ?, " "last_name = ?, " @@ -19293,8 +19295,9 @@ bool CLuaBaseEntity::setChocoboRaisingInfo(const sol::table& table) "stage = ?, " "location = ?, " "color = ?, " - "dominant_gene = ?, " - "recessive_gene = ?, " + "allele1 = ?, " + "allele2 = ?, " + "allele3 = ?, " "strength = ?, " "endurance = ?, " "discernment = ?, " @@ -19309,7 +19312,34 @@ bool CLuaBaseEntity::setChocoboRaisingInfo(const sol::table& table) "weather_preference = ?, " "hunger = ?, " "care_plan = ?, " - "held_item = ? "; + "held_item = ? " + "ON DUPLICATE KEY UPDATE " + "first_name = VALUES(first_name), " + "last_name = VALUES(last_name), " + "sex = VALUES(sex), " + "created = VALUES(created), " + "last_update_age = VALUES(last_update_age), " + "stage = VALUES(stage), " + "location = VALUES(location), " + "color = VALUES(color), " + "allele1 = VALUES(allele1), " + "allele2 = VALUES(allele2), " + "allele3 = VALUES(allele3), " + "strength = VALUES(strength), " + "endurance = VALUES(endurance), " + "discernment = VALUES(discernment), " + "receptivity = VALUES(receptivity), " + "affection = VALUES(affection), " + "energy = VALUES(energy), " + "satisfaction = VALUES(satisfaction), " + "conditions = VALUES(conditions), " + "ability1 = VALUES(ability1), " + "ability2 = VALUES(ability2), " + "personality = VALUES(personality), " + "weather_preference = VALUES(weather_preference), " + "hunger = VALUES(hunger), " + "care_plan = VALUES(care_plan), " + "held_item = VALUES(held_item);"; const auto rset = db::preparedStmt(Query, m_PBaseEntity->id, @@ -19321,8 +19351,9 @@ bool CLuaBaseEntity::setChocoboRaisingInfo(const sol::table& table) table.get_or("stage", 1), table.get_or("location", 0), table.get_or("color", 0), - table.get_or("dominant_gene", 0), - table.get_or("recessive_gene", 0), + table.get_or("allele1", 0), + table.get_or("allele2", 0), + table.get_or("allele3", 0), table.get_or("strength", 0), table.get_or("endurance", 0), table.get_or("discernment", 0), @@ -19341,7 +19372,7 @@ bool CLuaBaseEntity::setChocoboRaisingInfo(const sol::table& table) if (!rset) { - ShowDebug("REPLACE Query failed"); + ShowDebug("UPSERT Query failed"); return false; } diff --git a/tools/migrations/051_add_chocobo_alleles.py b/tools/migrations/051_add_chocobo_alleles.py new file mode 100644 index 00000000000..13a0cd9d9cf --- /dev/null +++ b/tools/migrations/051_add_chocobo_alleles.py @@ -0,0 +1,27 @@ +import mariadb + + +def migration_name(): + return "Adding allele columns to char_chocobos table" + + +def check_preconditions(cur): + return + + +def needs_to_run(cur): + cur.execute("SHOW COLUMNS FROM char_chocobos LIKE 'allele1'") + if not cur.fetchone(): + return True + return False + + +def migrate(cur, db): + try: + cur.execute("ALTER TABLE char_chocobos \ + ADD COLUMN `allele1` tinyint unsigned NOT NULL DEFAULT 0, \ + ADD COLUMN `allele2` tinyint unsigned NOT NULL DEFAULT 0, \ + ADD COLUMN `allele3` tinyint unsigned NOT NULL DEFAULT 0;") + db.commit() + except mariadb.Error as err: + print("Something went wrong: {}".format(err)) From 7a67757295ab80838bfad7d461bba13b5478585c Mon Sep 17 00:00:00 2001 From: Zach Toogood Date: Sat, 2 May 2026 17:08:53 +0100 Subject: [PATCH 2/2] Core: Turn REPLACE INTO into upserts --- src/map/char_recast_container.cpp | 18 +++- src/map/fishingcontest.cpp | 74 +++++++++++---- src/map/lua/lua_baseentity.cpp | 63 ++++++------- src/map/monstrosity.cpp | 98 ++++++++++++-------- src/map/utils/charutils.cpp | 146 +++++++++++++++++++++--------- 5 files changed, 262 insertions(+), 137 deletions(-) diff --git a/src/map/char_recast_container.cpp b/src/map/char_recast_container.cpp index 20e74a74539..8567bb64822 100644 --- a/src/map/char_recast_container.cpp +++ b/src/map/char_recast_container.cpp @@ -53,11 +53,19 @@ void CCharRecastContainer::Add(RECASTTYPE type, const Recast id, timer::duration if (type == RECAST_ABILITY) { - db::preparedStmt("REPLACE INTO char_recast VALUES (?, ?, ?, ?)", - m_PChar->id, - recast->ID, - earth_time::timestamp(timer::to_utc(recast->TimeStamp)), - static_cast(timer::count_seconds(recast->RecastTime))); + db::preparedStmt( + "INSERT INTO char_recast SET " + "charid = ?, " + "id = ?, " + "time = ?, " + "recast = ? " + "ON DUPLICATE KEY UPDATE " + "time = VALUES(time), " + "recast = VALUES(recast)", + m_PChar->id, + recast->ID, + earth_time::timestamp(timer::to_utc(recast->TimeStamp)), + static_cast(timer::count_seconds(recast->RecastTime))); } } diff --git a/src/map/fishingcontest.cpp b/src/map/fishingcontest.cpp index 0c9cd9b30f6..69b60f89a11 100644 --- a/src/map/fishingcontest.cpp +++ b/src/map/fishingcontest.cpp @@ -446,26 +446,64 @@ bool WriteContestEntryData(FishingContestEntry* entry) return false; } - // Update the DB with the current contest entries - const auto rset = db::preparedStmt("REPLACE INTO `fishing_contest_entries` " - "(charid, mjob, sjob, mlevel, slevel, race, allegiance, fishRank, score, submitTime, contestRank, share) " - "SELECT charid, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? " - "FROM chars WHERE charname = ?", - entry->mjob, - entry->sjob, - entry->mlvl, - entry->slvl, - entry->race, - entry->allegiance, - entry->fishRank, - entry->score, - entry->submitTime, - entry->contestRank, - entry->share, - entry->name); + const auto maybeCharId = [&]() -> Maybe + { + const auto rset = db::preparedStmt("SELECT charid FROM chars WHERE charname = ?", entry->name); + FOR_DB_SINGLE_RESULT(rset) + { + return rset->get("charid"); + } + return std::nullopt; + }(); + if (!maybeCharId) + { + ShowErrorFmt("Failed to look up {}", entry->name); + return false; + } + const auto charid = *maybeCharId; + + const auto rset = db::preparedStmt( + "INSERT INTO fishing_contest_entries SET " + "charid = ?, " + "mjob = ?, " + "sjob = ?, " + "mlevel = ?, " + "slevel = ?, " + "race = ?, " + "allegiance = ?, " + "fishRank = ?, " + "score = ?, " + "submitTime = ?, " + "contestRank = ?, " + "share = ? " + "ON DUPLICATE KEY UPDATE " + "mjob = VALUES(mjob), " + "sjob = VALUES(sjob), " + "mlevel = VALUES(mlevel), " + "slevel = VALUES(slevel), " + "race = VALUES(race), " + "allegiance = VALUES(allegiance), " + "fishRank = VALUES(fishRank), " + "score = VALUES(score), " + "submitTime = VALUES(submitTime), " + "contestRank = VALUES(contestRank), " + "share = VALUES(share)", + charid, + entry->mjob, + entry->sjob, + entry->mlvl, + entry->slvl, + entry->race, + entry->allegiance, + entry->fishRank, + entry->score, + entry->submitTime, + entry->contestRank, + entry->share); + if (!rset) { - ShowDebug("Error writing fishing contest data to database."); + ShowErrorFmt("Error writing fishing contest data to database {} ({}).", entry->name, charid); return false; } diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 853279e3a12..8ad0e2e2b90 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -19246,9 +19246,9 @@ auto CLuaBaseEntity::getChocoboRaisingInfo() -> sol::table table["location"] = rset->get("location"); table["color"] = rset->get("color"); - table["allele1"] = rset->get("allele1"); - table["allele2"] = rset->get("allele2"); - table["allele3"] = rset->get("allele3"); + table["allele1"] = rset->get("allele1"); + table["allele2"] = rset->get("allele2"); + table["allele3"] = rset->get("allele3"); table["strength"] = rset->get("strength"); table["endurance"] = rset->get("endurance"); @@ -19341,34 +19341,35 @@ bool CLuaBaseEntity::setChocoboRaisingInfo(const sol::table& table) "care_plan = VALUES(care_plan), " "held_item = VALUES(held_item);"; - const auto rset = db::preparedStmt(Query, - m_PBaseEntity->id, - table.get_or("first_name", "Chocobo"), - table.get_or("last_name", "Chocobo"), - table.get_or("sex", 0), - table.get_or("created", 0), - table.get_or("last_update_age", 0), - table.get_or("stage", 1), - table.get_or("location", 0), - table.get_or("color", 0), - table.get_or("allele1", 0), - table.get_or("allele2", 0), - table.get_or("allele3", 0), - table.get_or("strength", 0), - table.get_or("endurance", 0), - table.get_or("discernment", 0), - table.get_or("receptivity", 0), - table.get_or("affection", 0), - table.get_or("energy", 0), - table.get_or("satisfaction", 0), - table.get_or("conditions", 0), - table.get_or("ability1", 0), - table.get_or("ability2", 0), - table.get_or("personality", 0), - table.get_or("weather_preference", 0), - table.get_or("hunger", 0), - table.get_or("care_plan", 0), - table.get_or("held_item", 0)); + const auto rset = db::preparedStmt( + Query, + m_PBaseEntity->id, + table.get_or("first_name", "Chocobo"), + table.get_or("last_name", "Chocobo"), + table.get_or("sex", 0), + table.get_or("created", 0), + table.get_or("last_update_age", 0), + table.get_or("stage", 1), + table.get_or("location", 0), + table.get_or("color", 0), + table.get_or("allele1", 0), + table.get_or("allele2", 0), + table.get_or("allele3", 0), + table.get_or("strength", 0), + table.get_or("endurance", 0), + table.get_or("discernment", 0), + table.get_or("receptivity", 0), + table.get_or("affection", 0), + table.get_or("energy", 0), + table.get_or("satisfaction", 0), + table.get_or("conditions", 0), + table.get_or("ability1", 0), + table.get_or("ability2", 0), + table.get_or("personality", 0), + table.get_or("weather_preference", 0), + table.get_or("hunger", 0), + table.get_or("care_plan", 0), + table.get_or("held_item", 0)); if (!rset) { diff --git a/src/map/monstrosity.cpp b/src/map/monstrosity.cpp index 6ecf09cd45e..a881441b45f 100644 --- a/src/map/monstrosity.cpp +++ b/src/map/monstrosity.cpp @@ -224,45 +224,65 @@ void monstrosity::WriteMonstrosityData(CCharEntity* PChar) return; } - const char* query = "REPLACE INTO char_monstrosity SET " - "charid = ?, " - "current_monstrosity_id = ?, " - "current_monstrosity_species = ?, " - "current_monstrosity_name_prefix_1 = ?, " - "current_monstrosity_name_prefix_2 = ?, " - "current_exp = ?, " - "equip = ?, " - "levels = ?, " - "instincts = ?, " - "variants = ?, " - "belligerency = ?, " - "entry_x = ?, " - "entry_y = ?, " - "entry_z = ?, " - "entry_rot = ?, " - "entry_zone_id = ?, " - "entry_mjob = ?, " - "entry_sjob = ?"; - - db::preparedStmt(query, - PChar->id, - PChar->m_PMonstrosity->MonstrosityId, - PChar->m_PMonstrosity->Species, - PChar->m_PMonstrosity->NamePrefix1, - PChar->m_PMonstrosity->NamePrefix2, - PChar->m_PMonstrosity->CurrentExp, - PChar->m_PMonstrosity->EquippedInstincts, - PChar->m_PMonstrosity->levels, - PChar->m_PMonstrosity->instincts, - PChar->m_PMonstrosity->variants, - static_cast(PChar->m_PMonstrosity->Belligerency), - PChar->m_PMonstrosity->EntryPos.x, - PChar->m_PMonstrosity->EntryPos.y, - PChar->m_PMonstrosity->EntryPos.z, - PChar->m_PMonstrosity->EntryPos.rotation, - PChar->m_PMonstrosity->EntryZoneId, - PChar->m_PMonstrosity->EntryMainJob, - PChar->m_PMonstrosity->EntrySubJob); + const char* query = + "INSERT INTO char_monstrosity SET " + "charid = ?, " + "current_monstrosity_id = ?, " + "current_monstrosity_species = ?, " + "current_monstrosity_name_prefix_1 = ?, " + "current_monstrosity_name_prefix_2 = ?, " + "current_exp = ?, " + "equip = ?, " + "levels = ?, " + "instincts = ?, " + "variants = ?, " + "belligerency = ?, " + "entry_x = ?, " + "entry_y = ?, " + "entry_z = ?, " + "entry_rot = ?, " + "entry_zone_id = ?, " + "entry_mjob = ?, " + "entry_sjob = ? " + "ON DUPLICATE KEY UPDATE " + "current_monstrosity_id = VALUES(current_monstrosity_id), " + "current_monstrosity_species = VALUES(current_monstrosity_species), " + "current_monstrosity_name_prefix_1 = VALUES(current_monstrosity_name_prefix_1), " + "current_monstrosity_name_prefix_2 = VALUES(current_monstrosity_name_prefix_2), " + "current_exp = VALUES(current_exp), " + "equip = VALUES(equip), " + "levels = VALUES(levels), " + "instincts = VALUES(instincts), " + "variants = VALUES(variants), " + "belligerency = VALUES(belligerency), " + "entry_x = VALUES(entry_x), " + "entry_y = VALUES(entry_y), " + "entry_z = VALUES(entry_z), " + "entry_rot = VALUES(entry_rot), " + "entry_zone_id = VALUES(entry_zone_id), " + "entry_mjob = VALUES(entry_mjob), " + "entry_sjob = VALUES(entry_sjob)"; + + db::preparedStmt( + query, + PChar->id, + PChar->m_PMonstrosity->MonstrosityId, + PChar->m_PMonstrosity->Species, + PChar->m_PMonstrosity->NamePrefix1, + PChar->m_PMonstrosity->NamePrefix2, + PChar->m_PMonstrosity->CurrentExp, + PChar->m_PMonstrosity->EquippedInstincts, + PChar->m_PMonstrosity->levels, + PChar->m_PMonstrosity->instincts, + PChar->m_PMonstrosity->variants, + static_cast(PChar->m_PMonstrosity->Belligerency), + PChar->m_PMonstrosity->EntryPos.x, + PChar->m_PMonstrosity->EntryPos.y, + PChar->m_PMonstrosity->EntryPos.z, + PChar->m_PMonstrosity->EntryPos.rotation, + PChar->m_PMonstrosity->EntryZoneId, + PChar->m_PMonstrosity->EntryMainJob, + PChar->m_PMonstrosity->EntrySubJob); } void monstrosity::TryPopulateMonstrosityData(CCharEntity* PChar) diff --git a/src/map/utils/charutils.cpp b/src/map/utils/charutils.cpp index a619bd0b478..092ebc5c1ea 100644 --- a/src/map/utils/charutils.cpp +++ b/src/map/utils/charutils.cpp @@ -3052,30 +3052,61 @@ void SaveJobChangeGear(CCharEntity* PChar) uint16 ring2 = getEquipIdFromSlot(PChar, SLOT_RING2); uint16 back = getEquipIdFromSlot(PChar, SLOT_BACK); - db::preparedStmt("REPLACE INTO char_equip_saved SET " - "charid = ?, jobid = ?, main = ?, sub = ?, " - "ranged = ?, ammo = ?, head = ?, body = ?, " - "hands = ?, legs = ?, feet = ?, neck = ?, " - "waist = ?, ear1 = ?, ear2 = ?, ring1 = ?, " - "ring2 = ?, back = ?", - PChar->id, - PChar->GetMJob(), - main, - sub, - ranged, - ammo, - head, - body, - hands, - legs, - feet, - neck, - waist, - ear1, - ear2, - ring1, - ring2, - back); + db::preparedStmt( + "INSERT INTO char_equip_saved SET " + "charid = ?, " + "jobid = ?, " + "main = ?, " + "sub = ?, " + "ranged = ?, " + "ammo = ?, " + "head = ?, " + "body = ?, " + "hands = ?, " + "legs = ?, " + "feet = ?, " + "neck = ?, " + "waist = ?, " + "ear1 = ?, " + "ear2 = ?, " + "ring1 = ?, " + "ring2 = ?, " + "back = ? " + "ON DUPLICATE KEY UPDATE " + "main = VALUES(main), " + "sub = VALUES(sub), " + "ranged = VALUES(ranged), " + "ammo = VALUES(ammo), " + "head = VALUES(head), " + "body = VALUES(body), " + "hands = VALUES(hands), " + "legs = VALUES(legs), " + "feet = VALUES(feet), " + "neck = VALUES(neck), " + "waist = VALUES(waist), " + "ear1 = VALUES(ear1), " + "ear2 = VALUES(ear2), " + "ring1 = VALUES(ring1), " + "ring2 = VALUES(ring2), " + "back = VALUES(back)", + PChar->id, + PChar->GetMJob(), + main, + sub, + ranged, + ammo, + head, + body, + hands, + legs, + feet, + neck, + waist, + ear1, + ear2, + ring1, + ring2, + back); } void LoadJobChangeGear(CCharEntity* PChar) @@ -7746,26 +7777,53 @@ void WriteHistory(const CCharEntity* PChar) return; } - // Replace will also handle insert if it doesn't exist - db::preparedStmt("REPLACE INTO char_history " - "(charid, enemies_defeated, times_knocked_out, mh_entrances, joined_parties, joined_alliances, spells_cast, " - "abilities_used, ws_used, items_used, chats_sent, npc_interactions, battles_fought, gm_calls, distance_travelled) " - "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - PChar->id, - PChar->m_charHistory.enemiesDefeated, - PChar->m_charHistory.timesKnockedOut, - PChar->m_charHistory.mhEntrances, - PChar->m_charHistory.joinedParties, - PChar->m_charHistory.joinedAlliances, - PChar->m_charHistory.spellsCast, - PChar->m_charHistory.abilitiesUsed, - PChar->m_charHistory.wsUsed, - PChar->m_charHistory.itemsUsed, - PChar->m_charHistory.chatsSent, - PChar->m_charHistory.npcInteractions, - PChar->m_charHistory.battlesFought, - PChar->m_charHistory.gmCalls, - PChar->m_charHistory.distanceTravelled); + db::preparedStmt( + "INSERT INTO char_history SET " + "charid = ?, " + "enemies_defeated = ?, " + "times_knocked_out = ?, " + "mh_entrances = ?, " + "joined_parties = ?, " + "joined_alliances = ?, " + "spells_cast = ?, " + "abilities_used = ?, " + "ws_used = ?, " + "items_used = ?, " + "chats_sent = ?, " + "npc_interactions = ?, " + "battles_fought = ?, " + "gm_calls = ?, " + "distance_travelled = ? " + "ON DUPLICATE KEY UPDATE " + "enemies_defeated = VALUES(enemies_defeated), " + "times_knocked_out = VALUES(times_knocked_out), " + "mh_entrances = VALUES(mh_entrances), " + "joined_parties = VALUES(joined_parties), " + "joined_alliances = VALUES(joined_alliances), " + "spells_cast = VALUES(spells_cast), " + "abilities_used = VALUES(abilities_used), " + "ws_used = VALUES(ws_used), " + "items_used = VALUES(items_used), " + "chats_sent = VALUES(chats_sent), " + "npc_interactions = VALUES(npc_interactions), " + "battles_fought = VALUES(battles_fought), " + "gm_calls = VALUES(gm_calls), " + "distance_travelled = VALUES(distance_travelled)", + PChar->id, + PChar->m_charHistory.enemiesDefeated, + PChar->m_charHistory.timesKnockedOut, + PChar->m_charHistory.mhEntrances, + PChar->m_charHistory.joinedParties, + PChar->m_charHistory.joinedAlliances, + PChar->m_charHistory.spellsCast, + PChar->m_charHistory.abilitiesUsed, + PChar->m_charHistory.wsUsed, + PChar->m_charHistory.itemsUsed, + PChar->m_charHistory.chatsSent, + PChar->m_charHistory.npcInteractions, + PChar->m_charHistory.battlesFought, + PChar->m_charHistory.gmCalls, + PChar->m_charHistory.distanceTravelled); } uint8 getMaxItemLevel(CCharEntity* PChar)