diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab0d20a --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +Copr. Mitachi + +Pros: +- This follows the Ymir logic, there used to be a system that gave basic items to Chinese players, though it was never really used. +- You can spawn with the items already equipped and specify any slot number you want for each item. +- You can also set up count, bonuses and stones(sockets, so also duration) in the starter equipment. +- Since it happens during character creation, there's no risk of doing it before CItem or QuestManager is properly initialized, indeed, we don't even use them here. +- The function is called during PlayerCreate, so you don't need a quest flag to prevent giving items twice. It only triggers on character creation. +- It supports a lightweight reload (core based), so you can hot test or run a starter boost event without shutting down the server. + +Cons: +- The only annoying part is having to specify the exact slot indexes where items go, even for equipment. But hey, you only need to do it once. + + +USAGE: + The structure of the JSON file is straightforward. If you're not familiar with it, feel free to ask https://chat.openai.com. + Not in the mood to use it? No problem. I’ve already used it to generate a README for you, scroll down + +# Starter Items System – `give_basic_weapon.json` Usage Guide + +This file defines the initial equipment and items given **at character creation**, organized into three sections. + +## `common` +These items are given to **all characters**, regardless of race or job. + +```json +{ "vnum": 27003, "pos": 0, "equip": false } +``` + +| Field | Description | +|-----------|------------------------------------------------------------------------------------| +| `vnum` | The item ID (vnum) to give | +| `pos` | Inventory slot index | +| `equip` | If true, the item is placed in the equipment slot | +| `count` | (Optional) Item quantity. Defaults to 1. | +| `sockets` | (Optional) List of socket values — default is 3, but this depends on ITEM_SOCKET_MAX_NUM in your source code. | +| `attrs` | (Optional) List of attribute structs | + +--- + +## `shared` +These items are given to all characters but are **intended to be equipped**, such as accessories or armor. + +```json +{ "vnum": 15009, "pos": 2, "equip": true } +``` + +- Must specify a valid equipment position (e.g., 2 = wrist, 3 = shoes). +- `equip: true` places the item in the `EQUIPMENT` window. + +--- + +## `jobs` +Job-specific equipment (e.g., armor, weapons), indexed by job ID: + +| Job ID | Job | +|--------|----------| +| `0` | Warrior | +| `1` | Ninja | +| `2` | Sura | +| `3` | Shaman | + +Example: +```json +{ + "vnum": 11209, + "pos": 0, + "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 1, "value": 500 }, + { "type": 53, "value": 30 } + ] +} +``` + +--- + +## Notes + +- All positions (`pos`) must be **manually specified**. +- Equipment positions follow the same logic as Metin2's `EQUIPMENT` window. +- `sockets` must contain 0–2 (default is 3, but this depends on your `ITEM_SOCKET_MAX_NUM`) integers, eg. `[0, 0, 0]`. +- `attrs` must contain objects with: + - `type`: bonus ID (e.g. `1` = Max HP) + - `value`: bonus value + +--- + +## Reloading + +To reload the JSON without restarting the server, use the GM command: + +``` +/reload give_basic_weapon +``` +--- + +## Equipment Slot Index Reference + +To determine the correct `pos` value for items marked as `"equip": true`, refer to the slot indices defined in `common/length.h`: + +This is an example but check yours, last wear indexes may be different. + +```cpp +enum EWearPositions +{ + WEAR_BODY = 0, + WEAR_HEAD = 1, + WEAR_FOOTS = 2, + WEAR_WRIST = 3, + WEAR_WEAPON = 4, + WEAR_NECK = 5, + WEAR_EAR = 6, + WEAR_UNIQUE1 = 7, + WEAR_UNIQUE2 = 8, + WEAR_ARROW = 9, + WEAR_SHIELD = 10, + + WEAR_ABILITY1 = 11, + WEAR_ABILITY2 = 12, + WEAR_ABILITY3 = 13, + WEAR_ABILITY4 = 14, + WEAR_ABILITY5 = 15, + WEAR_ABILITY6 = 16, + WEAR_ABILITY7 = 17, + WEAR_ABILITY8 = 18, + WEAR_COSTUME_BODY = 19, + WEAR_COSTUME_HAIR = 20, + + WEAR_COSTUME_MOUNT = 21, // if ENABLE_MOUNT_COSTUME + WEAR_COSTUME_ACCE = 22, // if ENABLE_ACCE_COSTUME + WEAR_COSTUME_WEAPON = 23, // if ENABLE_WEAPON_COSTUME + WEAR_RING1 = 24, + WEAR_RING2 = 25, + WEAR_BELT = 26, + + WEAR_MAX = 32, +}; +``` + +Use the corresponding `WEAR_XXX` value as the `pos` index for your equipment items in the JSON. + +--- + +## Json Errors? + +If you got some errors, use https://jsonformatter.org/, past the code and Validate. +It fill fix the file for you diff --git a/Share-Locale-Server/README.txt b/Share-Locale-Server/README.txt new file mode 100644 index 0000000..e63ba91 --- /dev/null +++ b/Share-Locale-Server/README.txt @@ -0,0 +1,2 @@ +In your locale server, like usr/game/share/locale/xx/, or, if you want, in the same directory of mob_drop_item.txt. +Add this file: give_basic_weapon.json diff --git a/Share-Locale-Server/give_basic_weapon.json b/Share-Locale-Server/give_basic_weapon.json new file mode 100644 index 0000000..03b4af7 --- /dev/null +++ b/Share-Locale-Server/give_basic_weapon.json @@ -0,0 +1,98 @@ +{ + "common": [ + { "vnum": 27003, "pos": 0, "equip": false }, + { "vnum": 27006, "pos": 1, "equip": false }, + { "vnum": 27102, "pos": 2, "equip": false, "count": 20 }, + { "vnum": 27105, "pos": 3, "equip": false, "count": 20 }, + { "vnum": 70038, "pos": 4, "equip": false } + ], + "shared": [ + { "vnum": 15009, "pos": 2, "equip": true }, + { "vnum": 14009, "pos": 3, "equip": true }, + { "vnum": 16009, "pos": 5, "equip": true }, + { "vnum": 17009, "pos": 6, "equip": true }, + { "vnum": 13009, "pos": 10, "equip": true } + ], + "jobs": { + "0": [ + { "vnum": 11209, "pos": 0, "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 1, "value": 500 }, + { "type": 53, "value": 30 } + ] + }, + { "vnum": 12209, "pos": 1, "equip": true }, + { + "vnum": 19, + "pos": 4, + "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 5, "value": 8 }, + { "type": 17, "value": 6 } + ] + } + ], + "1": [ + { "vnum": 11409, "pos": 0, "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 1, "value": 500 }, + { "type": 53, "value": 30 } + ] + }, + { "vnum": 12349, "pos": 1, "equip": true }, + { + "vnum": 1009, + "pos": 4, + "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 5, "value": 8 }, + { "type": 17, "value": 6 } + ] + } + ], + "2": [ + { "vnum": 11609, "pos": 0, "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 1, "value": 500 }, + { "type": 53, "value": 30 } + ] + }, + { "vnum": 12489, "pos": 1, "equip": true }, + { + "vnum": 19, + "pos": 4, + "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 5, "value": 8 }, + { "type": 17, "value": 6 } + ] + } + ], + "3": [ + { "vnum": 11809, "pos": 0, "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 1, "value": 500 }, + { "type": 53, "value": 30 } + ] + }, + { "vnum": 12629, "pos": 1, "equip": true }, + { + "vnum": 7009, + "pos": 4, + "equip": true, + "sockets": [0, 0, 0], + "attrs": [ + { "type": 5, "value": 8 }, + { "type": 17, "value": 6 } + ] + } + ] + } +} diff --git a/Srcs-Server/common/service.h or CommonDefines.h or macroDefines.h b/Srcs-Server/common/service.h or CommonDefines.h or macroDefines.h new file mode 100644 index 0000000..7626b9a --- /dev/null +++ b/Srcs-Server/common/service.h or CommonDefines.h or macroDefines.h @@ -0,0 +1,3 @@ +// Add + +#define ENABLE_START_ITEMS diff --git a/Srcs-Server/game/src/#new files to add/start_item.cpp b/Srcs-Server/game/src/#new files to add/start_item.cpp new file mode 100644 index 0000000..c90accd --- /dev/null +++ b/Srcs-Server/game/src/#new files to add/start_item.cpp @@ -0,0 +1,95 @@ +#include "stdafx.h" + +#include "start_item.h" + +#include +#include + +std::vector g_vecCommonItems; +std::vector g_vecSharedItems; +std::unordered_map> g_umapJobItems; + +bool LoadStarterItemsFromJSON() +{ + const std::string strPath = LocaleService_GetBasePath() + "/give_basic_weapon.json"; + std::ifstream ifsFile(strPath); + if (!ifsFile.is_open()) + { + sys_err("StarterItems: Failed to open %s", strPath.c_str()); + return false; + } + + nlohmann::json jRoot; + try + { + ifsFile >> jRoot; + } + catch (const std::exception& e) + { + sys_err("StarterItems: JSON parse error: %s", e.what()); + return false; + } + + // validate required fields + if (!jRoot.contains("common") || !jRoot.contains("shared") || !jRoot.contains("jobs")) + { + sys_err("StarterItems: Missing required sections in JSON"); + return false; + } + + std::vector vecCommonItems; + std::vector vecSharedItems; + std::unordered_map> umapJobItems; + + auto fnParseItem = [](const nlohmann::json& jItem) -> BaseItem { + BaseItem stItem; + stItem.vnum = jItem["vnum"]; + stItem.pos = jItem["pos"]; + stItem.is_equipment = jItem["equip"]; + stItem.count = jItem.value("count", 1); + + if (jItem.contains("sockets") && jItem["sockets"].is_array()) + { + const auto& jSockets = jItem["sockets"]; + for (size_t i = 0; i < MIN(jSockets.size(), stItem.sockets.size()); ++i) + stItem.sockets[i] = jSockets[i]; + } + + if (jItem.contains("attrs") && jItem["attrs"].is_array()) + { + const auto& jAttrs = jItem["attrs"]; + for (size_t i = 0; i < MIN(jAttrs.size(), stItem.attrs.size()); ++i) + { + stItem.attrs[i].bType = jAttrs[i].value("type", 0); + stItem.attrs[i].sValue = jAttrs[i].value("value", 0); + } + } + + return stItem; + }; + + for (const auto& jItem : jRoot["common"]) + vecCommonItems.emplace_back(fnParseItem(jItem)); + + for (const auto& jItem : jRoot["shared"]) + vecSharedItems.emplace_back(fnParseItem(jItem)); + + for (const auto& [strKey, jItems] : jRoot["jobs"].items()) + { + BYTE byJob = static_cast(std::stoi(strKey)); + if (!jItems.is_array()) + continue; + + auto& vecJob = umapJobItems[byJob]; + for (const auto& jItem : jItems) + vecJob.emplace_back(fnParseItem(jItem)); + } + + // commit parsed items + g_vecCommonItems = std::move(vecCommonItems); + g_vecSharedItems = std::move(vecSharedItems); + g_umapJobItems = std::move(umapJobItems); + + sys_log(0, "StarterItems: Loaded successfully from %s", strPath.c_str()); + return true; +} diff --git a/Srcs-Server/game/src/#new files to add/start_item.h b/Srcs-Server/game/src/#new files to add/start_item.h new file mode 100644 index 0000000..cd1101b --- /dev/null +++ b/Srcs-Server/game/src/#new files to add/start_item.h @@ -0,0 +1,20 @@ +#pragma once +#include "locale_service.h" + +#include "../../common/tables.h" +#include "../../common/item_length.h" + +struct BaseItem +{ + DWORD vnum; + BYTE pos; + bool is_equipment; + BYTE count{1}; + std::array sockets{}; + std::array attrs{}; +}; + +extern std::vector g_vecCommonItems; +extern std::vector g_vecSharedItems; +extern std::unordered_map> g_umapJobItems; +extern bool LoadStarterItemsFromJSON(); diff --git a/Srcs-Server/game/src/Makefile b/Srcs-Server/game/src/Makefile new file mode 100644 index 0000000..6d073bc --- /dev/null +++ b/Srcs-Server/game/src/Makefile @@ -0,0 +1,7 @@ +## 1.) Look for + + DragonSoul.cpp + +# replace with: + + DragonSoul.cpp start_item.cpp diff --git a/Srcs-Server/game/src/cmd_gm.cpp b/Srcs-Server/game/src/cmd_gm.cpp new file mode 100644 index 0000000..74baec7 --- /dev/null +++ b/Srcs-Server/game/src/cmd_gm.cpp @@ -0,0 +1,37 @@ +/// 1.) Search at the begin of the file + +#include "unique_item.h" +#include "DragonSoul.h" + +// and add under, this: + +#ifdef ENABLE_START_ITEMS +#include "start_item.h" +#endif + +/// 2.) Search in void CHARACTER::MountVnum(DWORD vnum): + + //RELOAD_ADMIN + case 'a': + ch->ChatPacket(CHAT_TYPE_INFO, "Reloading Admin infomation."); + db_clientdesc->DBPacket(HEADER_GD_RELOAD_ADMIN, 0, NULL, 0); + sys_log(0, "Reloading admin infomation."); + break; + //END_RELOAD_ADMIN + case 'c': // cube + // ·ÎÄà ÇÁ·Î¼¼½º¸¸ °»»êÇÑ´Ù. + Cube_init (); + break; + + default: + { + +// and add under, this: + + #ifdef ENABLE_START_ITEMS + if (strstr(arg1, "give_basic_weapon")) + { + ch->ChatPacket(CHAT_TYPE_INFO, "Reloading: give_basic_weapon"); + LoadStarterItemsFromJSON(); + } + #endif diff --git a/Srcs-Server/game/src/input_db.cpp b/Srcs-Server/game/src/input_db.cpp new file mode 100644 index 0000000..12f86e1 --- /dev/null +++ b/Srcs-Server/game/src/input_db.cpp @@ -0,0 +1,111 @@ +/// 1.) Search at the begin of the file + +#include "map_location.h" + +#include "DragonSoul.h" + +// and add under, this: + +#ifdef ENABLE_START_ITEMS +#include "start_item.h" +#endif + +/// 2.) Scroll a bit down and look for + +extern void gm_insert(const char * name, BYTE level); +extern BYTE gm_get_level(const char * name, const char * host, const char* account); +extern void gm_host_insert(const char * host); + +// and add under, this: + +#ifdef ENABLE_START_ITEMS +extern bool RaceToJob(unsigned race, unsigned* ret_job); +#endif + +/// 3.) Search: + +void CInputDB::PlayerCreateSuccess(LPDESC d, const char * data) +{ + [...] +} + +// and add UPPER, this: + +#ifdef ENABLE_START_ITEMS +static void PlayerCreateGiveBasicItems(const DWORD dwPlayerID, const BYTE byJob) +{ + // extract real job + unsigned int real_job = 0; + RaceToJob(byJob, &real_job); + + // helper lambda to give items + auto fnGiveItem = [&](const BaseItem& item) + { + if (!item.vnum) + return; + + TPlayerItem t{}; + t.id = ITEM_MANAGER::instance().GetNewID(); + t.owner = dwPlayerID; + t.window = item.is_equipment ? EQUIPMENT : INVENTORY; + t.pos = item.pos; + t.vnum = item.vnum; + t.count = item.count; + + std::copy(item.sockets.begin(), item.sockets.end(), t.alSockets); + std::copy(item.attrs.begin(), item.attrs.end(), t.aAttr); + + db_clientdesc->DBPacketHeader(HEADER_GD_ITEM_SAVE, 0, sizeof(t)); + db_clientdesc->Packet(&t, sizeof(t)); + }; + + // items giving + for (const auto& item : g_vecCommonItems) + fnGiveItem(item); + + for (const auto& item : g_vecSharedItems) + fnGiveItem(item); + + if (auto it = g_umapJobItems.find(real_job); it != g_umapJobItems.end()) + { + for (const auto& item : it->second) + fnGiveItem(item); + } +} +#endif + +/// 3.) Search always inside void CInputDB::PlayerCreateSuccess(LPDESC d, const char * data): + + TPacketGCPlayerCreateSuccess pack{}; + + pack.header = HEADER_GC_CHARACTER_CREATE_SUCCESS; + pack.bAccountCharacterIndex = pPacketDB->bAccountCharacterIndex; + pack.player = pPacketDB->player; + + d->Packet(&pack, sizeof(TPacketGCPlayerCreateSuccess)); + + +// and add under (like before the function end), this: + + #ifdef ENABLE_START_ITEMS + // Base items + PlayerCreateGiveBasicItems(pPacketDB->player.dwID, pPacketDB->player.byJob); + #endif + +// like: +/* + .... + pack.header = HEADER_GC_CHARACTER_CREATE_SUCCESS; + pack.bAccountCharacterIndex = pPacketDB->bAccountCharacterIndex; + pack.player = pPacketDB->player; + + d->Packet(&pack, sizeof(TPacketGCPlayerCreateSuccess)); + + #ifdef ENABLE_START_ITEMS + // Base items + PlayerCreateGiveBasicItems(pPacketDB->player.dwID, pPacketDB->player.byJob); + #endif + + LogManager::instance().CharLog(pack.player.dwID, 0, 0, 0, "CREATE PLAYER", "", d->GetHostName()); +} +*/ diff --git a/Srcs-Server/game/src/main.cpp b/Srcs-Server/game/src/main.cpp new file mode 100644 index 0000000..1b80be0 --- /dev/null +++ b/Srcs-Server/game/src/main.cpp @@ -0,0 +1,22 @@ +/// 1.) Search at the begin of the file + +#include "skill_power.h" +#include "DragonSoul.h" + +// and add under, this: + +#ifdef ENABLE_START_ITEMS +#include "start_item.h" +#endif + +/// 2.) Search inside int main(int argc, char **argv): + + Blend_Item_init(); + ani_init(); + PanamaLoad(); + +// and add under, this: + + #ifdef ENABLE_START_ITEMS + LoadStarterItemsFromJSON(); + #endif