diff --git a/changelog.txt b/changelog.txt index d6bae58d2..dc0223d52 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,17 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 1/24/2019 == +Uleat: Extended server spellbook entries to RoF2 standard and added per-client restriction of spell id max + - Bumped server spellbook entry capacity to 720 spells + - Server keeps all 'learned' spells as found + -- Access is limited by the clients' limitations of spellbook capacities and max spell ids + -- This is done to avoid losing spells by switching from newer clients to older ones + -- Existing behavior is kept in place for illegal access conditions + - Each client is still restricted to its spellbook capacity (400, 480, 480, 720, 720, 720 - respectively) + - Each client is restricted to its max supported spell id (9999, 15999, 23000, 28000, 45000, 45000 - respectively) + - Please report any abnormal behavior so it may be addressed + == 1/20/2019 == Uleat: Added 'spells' entry to EQDictionary Akkadius: diff --git a/common/emu_constants.h b/common/emu_constants.h index 6fe6f8ec6..e3869570e 100644 --- a/common/emu_constants.h +++ b/common/emu_constants.h @@ -243,7 +243,7 @@ namespace EQEmu }; using RoF2::spells::SPELL_ID_MAX; - using SoD::spells::SPELLBOOK_SIZE; + using RoF2::spells::SPELLBOOK_SIZE; using UF::spells::SPELL_GEM_COUNT; // RoF+ clients define more than UF client..but, they are not valid beyond UF using RoF2::spells::LONG_BUFFS; diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 8e0d4d0d1..a5d86d140 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -1091,6 +1091,18 @@ struct PlayerProfile_Struct /*19559*/ uint8 unknown19595[5]; // ***Placeholder (6/29/2005) /*19564*/ uint32 RestTimer; /*19568*/ + + // All player profile packets are translated and this overhead is ignored in out-bound packets + PlayerProfile_Struct() : m_player_profile_version(EQEmu::versions::MobVersion::Unknown) { } + + EQEmu::versions::MobVersion PlayerProfileVersion() { return m_player_profile_version; } + void SetPlayerProfileVersion(EQEmu::versions::MobVersion mob_version) { m_player_profile_version = EQEmu::versions::ValidateMobVersion(mob_version); } + void SetPlayerProfileVersion(EQEmu::versions::ClientVersion client_version) { SetPlayerProfileVersion(EQEmu::versions::ConvertClientVersionToMobVersion(client_version)); } + +// private: + // No need for gm flag since pp already has one + // No need for lookup pointer since this struct is not tied to any one system + EQEmu::versions::MobVersion m_player_profile_version; // kept public for now so checksum can calc sizeof (client_packet.cpp:1586) }; diff --git a/common/patches/rof.cpp b/common/patches/rof.cpp index 5a87748a4..be077d750 100644 --- a/common/patches/rof.cpp +++ b/common/patches/rof.cpp @@ -2126,14 +2126,25 @@ namespace RoF outapp->WriteUInt32(spells::SPELLBOOK_SIZE); // Spellbook slots - for (uint32 r = 0; r < EQEmu::spells::SPELLBOOK_SIZE; r++) - { - outapp->WriteUInt32(emu->spell_book[r]); + if (spells::SPELLBOOK_SIZE <= EQEmu::spells::SPELLBOOK_SIZE) { + for (uint32 r = 0; r < spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + outapp->WriteUInt32(emu->spell_book[r]); + else + outapp->WriteUInt32(0xFFFFFFFFU); + } } - // zeroes for the rest of the spellbook slots - for (uint32 r = 0; r < spells::SPELLBOOK_SIZE - EQEmu::spells::SPELLBOOK_SIZE; r++) - { - outapp->WriteUInt32(0xFFFFFFFFU); + else { + for (uint32 r = 0; r < EQEmu::spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + outapp->WriteUInt32(emu->spell_book[r]); + else + outapp->WriteUInt32(0xFFFFFFFFU); + } + // invalidate the rest of the spellbook slots + for (uint32 r = EQEmu::spells::SPELLBOOK_SIZE; r < spells::SPELLBOOK_SIZE; r++) { + outapp->WriteUInt32(0xFFFFFFFFU); + } } outapp->WriteUInt32(spells::SPELL_GEM_COUNT); // Memorised spell slots diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 74453f1ff..33af8506f 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -2202,14 +2202,25 @@ namespace RoF2 outapp->WriteUInt32(spells::SPELLBOOK_SIZE); // Spellbook slots - for (uint32 r = 0; r < EQEmu::spells::SPELLBOOK_SIZE; r++) - { - outapp->WriteUInt32(emu->spell_book[r]); + if (spells::SPELLBOOK_SIZE <= EQEmu::spells::SPELLBOOK_SIZE) { + for (uint32 r = 0; r < spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + outapp->WriteUInt32(emu->spell_book[r]); + else + outapp->WriteUInt32(0xFFFFFFFFU); + } } - // zeroes for the rest of the spellbook slots - for (uint32 r = 0; r < spells::SPELLBOOK_SIZE - EQEmu::spells::SPELLBOOK_SIZE; r++) - { - outapp->WriteUInt32(0xFFFFFFFFU); + else { + for (uint32 r = 0; r < EQEmu::spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + outapp->WriteUInt32(emu->spell_book[r]); + else + outapp->WriteUInt32(0xFFFFFFFFU); + } + // invalidate the rest of the spellbook slots + for (uint32 r = EQEmu::spells::SPELLBOOK_SIZE; r < spells::SPELLBOOK_SIZE; r++) { + outapp->WriteUInt32(0xFFFFFFFFU); + } } outapp->WriteUInt32(spells::SPELL_GEM_COUNT); // Memorised spell slots diff --git a/common/patches/sod.cpp b/common/patches/sod.cpp index e64510d06..1241e142a 100644 --- a/common/patches/sod.cpp +++ b/common/patches/sod.cpp @@ -1492,7 +1492,26 @@ namespace SoD OUT(WIS); OUT(face); // OUT(unknown02264[47]); - OUT_array(spell_book, spells::SPELLBOOK_SIZE); + + if (spells::SPELLBOOK_SIZE <= EQEmu::spells::SPELLBOOK_SIZE) { + for (uint32 r = 0; r < spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + eq->spell_book[r] = emu->spell_book[r]; + else + eq->spell_book[r] = 0xFFFFFFFFU; + } + } + else { + for (uint32 r = 0; r < EQEmu::spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + eq->spell_book[r] = emu->spell_book[r]; + else + eq->spell_book[r] = 0xFFFFFFFFU; + } + // invalidate the rest of the spellbook slots + memset(&eq->spell_book[EQEmu::spells::SPELLBOOK_SIZE], 0xFF, (sizeof(uint32) * (spells::SPELLBOOK_SIZE - EQEmu::spells::SPELLBOOK_SIZE))); + } + // OUT(unknown4184[128]); OUT_array(mem_spells, spells::SPELL_GEM_COUNT); // OUT(unknown04396[32]); diff --git a/common/patches/sof.cpp b/common/patches/sof.cpp index a1141f99c..247912dfd 100644 --- a/common/patches/sof.cpp +++ b/common/patches/sof.cpp @@ -1156,7 +1156,26 @@ namespace SoF OUT(WIS); OUT(face); // OUT(unknown02264[47]); - OUT_array(spell_book, spells::SPELLBOOK_SIZE); + + if (spells::SPELLBOOK_SIZE <= EQEmu::spells::SPELLBOOK_SIZE) { + for (uint32 r = 0; r < spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + eq->spell_book[r] = emu->spell_book[r]; + else + eq->spell_book[r] = 0xFFFFFFFFU; + } + } + else { + for (uint32 r = 0; r < EQEmu::spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + eq->spell_book[r] = emu->spell_book[r]; + else + eq->spell_book[r] = 0xFFFFFFFFU; + } + // invalidate the rest of the spellbook slots + memset(&eq->spell_book[EQEmu::spells::SPELLBOOK_SIZE], 0xFF, (sizeof(uint32) * (spells::SPELLBOOK_SIZE - EQEmu::spells::SPELLBOOK_SIZE))); + } + // OUT(unknown4184[128]); OUT_array(mem_spells, spells::SPELL_GEM_COUNT); // OUT(unknown04396[32]); diff --git a/common/patches/titanium.cpp b/common/patches/titanium.cpp index 9220875a8..e15913669 100644 --- a/common/patches/titanium.cpp +++ b/common/patches/titanium.cpp @@ -1012,7 +1012,26 @@ namespace Titanium OUT(WIS); OUT(face); // OUT(unknown02264[47]); - OUT_array(spell_book, spells::SPELLBOOK_SIZE); + + if (spells::SPELLBOOK_SIZE <= EQEmu::spells::SPELLBOOK_SIZE) { + for (uint32 r = 0; r < spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + eq->spell_book[r] = emu->spell_book[r]; + else + eq->spell_book[r] = 0xFFFFFFFFU; + } + } + else { + for (uint32 r = 0; r < EQEmu::spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + eq->spell_book[r] = emu->spell_book[r]; + else + eq->spell_book[r] = 0xFFFFFFFFU; + } + // invalidate the rest of the spellbook slots + memset(&eq->spell_book[EQEmu::spells::SPELLBOOK_SIZE], 0xFF, (sizeof(uint32) * (spells::SPELLBOOK_SIZE - EQEmu::spells::SPELLBOOK_SIZE))); + } + // OUT(unknown4184[448]); OUT_array(mem_spells, spells::SPELL_GEM_COUNT); // OUT(unknown04396[32]); diff --git a/common/patches/uf.cpp b/common/patches/uf.cpp index 9e87a7579..549fa7698 100644 --- a/common/patches/uf.cpp +++ b/common/patches/uf.cpp @@ -1724,8 +1724,26 @@ namespace UF OUT(WIS); OUT(face); // OUT(unknown02264[47]); - memset(eq->spell_book, 0xFF, sizeof(uint32)* spells::SPELLBOOK_SIZE); - OUT_array(spell_book, 480U); + + if (spells::SPELLBOOK_SIZE <= EQEmu::spells::SPELLBOOK_SIZE) { + for (uint32 r = 0; r < spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + eq->spell_book[r] = emu->spell_book[r]; + else + eq->spell_book[r] = 0xFFFFFFFFU; + } + } + else { + for (uint32 r = 0; r < EQEmu::spells::SPELLBOOK_SIZE; r++) { + if (emu->spell_book[r] <= spells::SPELL_ID_MAX) + eq->spell_book[r] = emu->spell_book[r]; + else + eq->spell_book[r] = 0xFFFFFFFFU; + } + // invalidate the rest of the spellbook slots + memset(&eq->spell_book[EQEmu::spells::SPELLBOOK_SIZE], 0xFF, (sizeof(uint32) * (spells::SPELLBOOK_SIZE - EQEmu::spells::SPELLBOOK_SIZE))); + } + // OUT(unknown4184[128]); OUT_array(mem_spells, spells::SPELL_GEM_COUNT); // OUT(unknown04396[32]); diff --git a/world/client.cpp b/world/client.cpp index 822b648b6..06bceb52d 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -1446,6 +1446,7 @@ bool Client::OPCharCreate(char *name, CharCreate_Struct *cc) ExtendedProfile_Struct ext; EQEmu::InventoryProfile inv; + pp.SetPlayerProfileVersion(EQEmu::versions::ConvertClientVersionToMobVersion(EQEmu::versions::ConvertClientVersionBitToClientVersion(m_ClientVersionBit))); inv.SetInventoryVersion(EQEmu::versions::ConvertClientVersionBitToClientVersion(m_ClientVersionBit)); inv.SetGMInventory(false); // character cannot have gm flag at this point @@ -1528,12 +1529,9 @@ bool Client::OPCharCreate(char *name, CharCreate_Struct *cc) // strcpy(pp.servername, WorldConfig::get()->ShortName.c_str()); - - for (i = 0; i < EQEmu::spells::SPELLBOOK_SIZE; i++) - pp.spell_book[i] = 0xFFFFFFFF; - - for(i = 0; i < EQEmu::spells::SPELL_GEM_COUNT; i++) - pp.mem_spells[i] = 0xFFFFFFFF; + memset(pp.spell_book, 0xFF, (sizeof(uint32) * EQEmu::spells::SPELLBOOK_SIZE)); + + memset(pp.mem_spells, 0xFF, (sizeof(uint32) * EQEmu::spells::SPELL_GEM_COUNT)); for(i = 0; i < BUFF_COUNT; i++) pp.buffs[i].spellid = 0xFFFF; diff --git a/world/worlddb.cpp b/world/worlddb.cpp index 4daa7fa77..47354cc60 100644 --- a/world/worlddb.cpp +++ b/world/worlddb.cpp @@ -98,6 +98,7 @@ void WorldDatabase::GetCharSelectInfo(uint32 accountID, EQApplicationPacket **ou PlayerProfile_Struct pp; EQEmu::InventoryProfile inv; + pp.SetPlayerProfileVersion(EQEmu::versions::ConvertClientVersionToMobVersion(client_version)); inv.SetInventoryVersion(client_version); inv.SetGMInventory(true); // charsel can not interact with items..but, no harm in setting to full expansion support diff --git a/zone/client.cpp b/zone/client.cpp index b0eca3c43..5077e5d53 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -2640,6 +2640,11 @@ bool Client::CheckAccess(int16 iDBLevel, int16 iDefaultLevel) { } void Client::MemorizeSpell(uint32 slot,uint32 spellid,uint32 scribing){ + if (slot < 0 || slot >= EQEmu::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize) + return; + if (spellid < 3 || spellid > EQEmu::spells::DynamicLookup(ClientVersion(), GetGM())->SpellIdMax) + return; + auto outapp = new EQApplicationPacket(OP_MemorizeSpell, sizeof(MemorizeSpell_Struct)); MemorizeSpell_Struct* mss=(MemorizeSpell_Struct*)outapp->pBuffer; mss->scribing=scribing; @@ -9126,4 +9131,4 @@ bool Client::GotoPlayer(std::string player_name) } return false; -} \ No newline at end of file +} diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 7485d7451..7ea18206a 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -530,10 +530,14 @@ void Client::CompleteConnect() SendAppearancePacket(AT_GuildID, GuildID(), false); SendAppearancePacket(AT_GuildRank, rank, false); } - for (uint32 spellInt = 0; spellInt < EQEmu::spells::SPELLBOOK_SIZE; spellInt++) { - if (m_pp.spell_book[spellInt] < 3 || m_pp.spell_book[spellInt] > 50000) + + // moved to dbload and translators since we iterate there also .. keep m_pp values whatever they are when they get here + /*const auto sbs = EQEmu::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize; + for (uint32 spellInt = 0; spellInt < sbs; ++spellInt) { + if (m_pp.spell_book[spellInt] < 3 || m_pp.spell_book[spellInt] > EQEmu::spells::SPELL_ID_MAX) m_pp.spell_book[spellInt] = 0xFFFFFFFF; - } + }*/ + //SendAATable(); if (GetHideMe()) Message(13, "[GM] You are currently hidden to all clients"); @@ -1154,6 +1158,7 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) SetClientVersion(Connection()->ClientVersion()); m_ClientVersionBit = EQEmu::versions::ConvertClientVersionToClientVersionBit(Connection()->ClientVersion()); + m_pp.SetPlayerProfileVersion(m_ClientVersion); m_inv.SetInventoryVersion(m_ClientVersion); /* Antighost code @@ -1587,8 +1592,8 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) if ((m_pp.RestTimer > RuleI(Character, RestRegenTimeToActivate)) && (m_pp.RestTimer > RuleI(Character, RestRegenRaidTimeToActivate))) m_pp.RestTimer = 0; - /* This checksum should disappear once dynamic structs are in... each struct strategy will do it */ - CRC32::SetEQChecksum((unsigned char*)&m_pp, sizeof(PlayerProfile_Struct) - 4); + /* This checksum should disappear once dynamic structs are in... each struct strategy will do it */ // looks to be in place now + CRC32::SetEQChecksum((unsigned char*)&m_pp, sizeof(PlayerProfile_Struct) - sizeof(m_pp.m_player_profile_version) - 4); outapp = new EQApplicationPacket(OP_PlayerProfile, sizeof(PlayerProfile_Struct)); @@ -5263,7 +5268,7 @@ void Client::Handle_OP_DeleteSpell(const EQApplicationPacket *app) EQApplicationPacket* outapp = app->Copy(); DeleteSpell_Struct* dss = (DeleteSpell_Struct*)outapp->pBuffer; - if (dss->spell_slot < 0 || dss->spell_slot > int(EQEmu::spells::SPELLBOOK_SIZE)) + if (dss->spell_slot < 0 || dss->spell_slot >= EQEmu::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize) return; if (m_pp.spell_book[dss->spell_slot] != SPELLBOOK_UNKNOWN) { @@ -13337,7 +13342,10 @@ void Client::Handle_OP_SwapSpell(const EQApplicationPacket *app) const SwapSpell_Struct* swapspell = (const SwapSpell_Struct*)app->pBuffer; int swapspelltemp; - if (swapspell->from_slot < 0 || swapspell->from_slot > EQEmu::spells::SPELLBOOK_SIZE || swapspell->to_slot < 0 || swapspell->to_slot > EQEmu::spells::SPELLBOOK_SIZE) + const auto sbs = EQEmu::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize; + if (swapspell->from_slot < 0 || swapspell->from_slot >= sbs) + return; + if (swapspell->to_slot < 0 || swapspell->to_slot >= sbs) return; swapspelltemp = m_pp.spell_book[swapspell->from_slot]; diff --git a/zone/command.cpp b/zone/command.cpp index 8c64c44a4..0590b16ea 100755 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -6441,7 +6441,15 @@ void command_scribespells(Client *c, const Seperator *sep) c->Message(0, "Scribing spells for %s.", t->GetName()); Log(Logs::General, Logs::Normal, "Scribe spells request for %s from %s, levels: %u -> %u", t->GetName(), c->GetName(), min_level, max_level); - for(curspell = 0, book_slot = t->GetNextAvailableSpellBookSlot(), count = 0; curspell < SPDAT_RECORDS && book_slot < EQEmu::spells::SPELLBOOK_SIZE; curspell++, book_slot = t->GetNextAvailableSpellBookSlot(book_slot)) + for ( + curspell = 0, + book_slot = t->GetNextAvailableSpellBookSlot(), + count = 0; // ; + curspell < SPDAT_RECORDS && + book_slot < EQEmu::spells::SPELLBOOK_SIZE; // ; + curspell++, + book_slot = t->GetNextAvailableSpellBookSlot(book_slot) + ) { if ( diff --git a/zone/questmgr.cpp b/zone/questmgr.cpp index 660ac3273..1c93eb3ed 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -984,8 +984,15 @@ uint16 QuestManager::scribespells(uint8 max_level, uint8 min_level) { bool SpellGlobalCheckResult = 0; bool SpellBucketCheckResult = 0; - - for(spell_id = 0, book_slot = initiator->GetNextAvailableSpellBookSlot(), count = 0; spell_id < SPDAT_RECORDS && book_slot < EQEmu::spells::SPELLBOOK_SIZE; spell_id++, book_slot = initiator->GetNextAvailableSpellBookSlot(book_slot)) + for ( + spell_id = 0, + book_slot = initiator->GetNextAvailableSpellBookSlot(), + count = 0; // ; + spell_id < SPDAT_RECORDS && + book_slot < EQEmu::spells::SPELLBOOK_SIZE; // ; + spell_id++, + book_slot = initiator->GetNextAvailableSpellBookSlot(book_slot) + ) { if ( diff --git a/zone/spells.cpp b/zone/spells.cpp index c88522a57..296097376 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -5053,7 +5053,7 @@ void Client::UnscribeSpell(int slot, bool update_client) m_pp.spell_book[slot] = 0xFFFFFFFF; database.DeleteCharacterSpell(this->CharacterID(), m_pp.spell_book[slot], slot); - if(update_client) + if(update_client && slot < EQEmu::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize) { auto outapp = new EQApplicationPacket(OP_DeleteSpell, sizeof(DeleteSpell_Struct)); DeleteSpell_Struct* del = (DeleteSpell_Struct*)outapp->pBuffer; @@ -5066,9 +5066,7 @@ void Client::UnscribeSpell(int slot, bool update_client) void Client::UnscribeSpellAll(bool update_client) { - int i; - - for(i = 0; i < EQEmu::spells::SPELLBOOK_SIZE; i++) + for(int i = 0; i < EQEmu::spells::SPELLBOOK_SIZE; i++) { if(m_pp.spell_book[i] != 0xFFFFFFFF) UnscribeSpell(i, update_client); diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 078fc7a4c..45786adbe 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -1225,17 +1225,28 @@ bool ZoneDatabase::LoadCharacterSpellBook(uint32 character_id, PlayerProfile_Str "`character_spells` " "WHERE `id` = %u ORDER BY `slot_id`", character_id); auto results = database.QueryDatabase(query); - int i = 0; + /* Initialize Spells */ - for (i = 0; i < EQEmu::spells::SPELLBOOK_SIZE; i++){ - pp->spell_book[i] = 0xFFFFFFFF; - } + + memset(pp->spell_book, 0xFF, (sizeof(uint32) * EQEmu::spells::SPELLBOOK_SIZE)); + + // We have the ability to block loaded spells by max id on a per-client basis.. + // but, we do not have to ability to keep players from using older clients after + // they have scribed spells on a newer one that exceeds the older one's limit. + // Load them all so that server actions are valid..but, nix them in translators. + for (auto row = results.begin(); row != results.end(); ++row) { - i = atoi(row[0]); - if (i < EQEmu::spells::SPELLBOOK_SIZE && atoi(row[1]) <= SPDAT_RECORDS){ - pp->spell_book[i] = atoi(row[1]); - } + int idx = atoi(row[0]); + int id = atoi(row[1]); + + if (idx < 0 || idx >= EQEmu::spells::SPELLBOOK_SIZE) + continue; + if (id < 3 || id > SPDAT_RECORDS) // 3 ("Summon Corpse") is the first scribable spell in spells_us.txt + continue; + + pp->spell_book[idx] = id; } + return true; }