diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2059eb59 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,190 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +### Building the project +```bash +# Standard release build +make + +# Debug build +make debug + +# Clean build artifacts +make clean +``` + +The project uses CMake. The Makefile wraps CMake commands for convenience: +- `make` - Builds with CMake in Release mode (`-DCMAKE_BUILD_TYPE=Release`) +- `make debug` - Builds with CMake in Debug mode (`-DCMAKE_BUILD_TYPE=Debug`) +- Build output binaries go to `bin/` directory +- Build output libraries go to `lib/` directory + +For development, always choose debug build! + +### Code formatting +```bash +# Format all C++ code +make fmt + +# Check format for modified files only (fast) +make fmt-check + +# Check format for all files (slow) +make fmt-check-all +``` + +The project uses clang-format with a `.clang-format` configuration file. +- Format checking is enforced via GitHub Actions on PRs +- Always run `make fmt` before committing changes + +## Project Architecture + +This is the **DarkEden** game server - an MMORPG server written in C++11. + +### Server Architecture + +The server is split into multiple coordinated processes: + +1. **loginserver** - Handles authentication and character selection +2. **sharedserver** - Manages shared data (e.g., guild info) across game servers +3. **gameserver** - The main game logic server (one per world/zone group) + +### Build System Structure + +- **CMake** is the primary build system (CMakeLists.txt files throughout) +- **Legacy Makefiles** exist in subdirectories but are superseded by CMake +- Source files are organized by module under `src/` + +### Key Directory Structure + +``` +src/ +├── Core/ # Core library - shared utilities, no server-type dependencies +│ ├── Packets/ # Protocol packet definitions (GC, CG, CL, LC, GL, LG, GS, SG, GG) +│ └── [core utilities] # Socket, datagram, player info, items, skills, etc. +├── server/ +│ ├── database/ # Database abstraction layer and connection management +│ ├── gameserver/ # Main game server executable +│ │ ├── skill/ # Skill system module +│ │ ├── item/ # Item system module +│ │ ├── billing/ # Billing/payment module +│ │ ├── war/ # War system module +│ │ ├── couple/ # Couple/party system module +│ │ ├── mission/ # Mission system module +│ │ ├── ctf/ # Capture the flag module +│ │ ├── quest/ # Quest system (with Lua scripting) +│ │ ├── mofus/ # Game events module +│ │ └── exchange/ # Player exchange/auction system +│ ├── loginserver/ # Login server executable +│ └── sharedserver/ # Shared server executable +└── Core/CMakeLists.txt # Defines packet libraries and Core library +``` + +### Packet System + +Packets are the primary communication mechanism between servers and clients. They are organized by direction: + +- **GC** (Game → Client): Server sends to client +- **CG** (Client → Game): Client sends to game server +- **LC** (Login → Client): Login server sends to client +- **CL** (Client → Login): Client sends to login server +- **GL** (Game → Login): Game server communicates with login server +- **LG** (Login → Game): Login server communicates with game server +- **GS** (Game → Shared): Game server communicates with shared server +- **SG** (Shared → Game): Shared server responds to game server +- **GG** (Game → Game): Inter-game-server communication + +Each packet type typically has two files: +- `PacketName.cpp` - Packet class definition +- `PacketNameHandler.cpp` - Handler that processes the packet + +### Preprocessor Macros + +Key compile definitions that control behavior: +- `__GAME_SERVER__` - Compiled for gameserver +- `__LOGIN_SERVER__` - Compiled for loginserver +- `__SHARED_SERVER__` - Compiled for sharedserver +- `__COMBAT__` - Enables combat-related code + +### Configuration + +Server configurations are in `conf/`: +- `gameserver.conf` - Game server configuration +- `loginserver.conf` - Login server configuration +- `sharedserver.conf` - Shared server configuration + +Important settings: +- `HomePath` - Repository directory path (must be set correctly) +- `DB_HOST` - Database IP address +- `LoginServerIP` - Login server IP + +**Note**: Database `WorldDBInfo` and `GameServerInfo` tables must match config file settings. + +## Database Setup + +The project requires MySQL 5.7 or 8 with specific SQL mode settings: + +```sql +-- Remove NO_ZERO_DATE and STRICT_TRANS_TABLES from sql_mode +set @@global.sql_mode = 'ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; +``` + +Databases: +- `DARKEDEN` - Main game database +- `USERINFO` - User account database + +Load schema with: +```bash +mysql -h 127.0.0.1 -u elcastle -D 'DARKEDEN' -p < initdb/DARKEDEN.sql +mysql -h 127.0.0.1 -u elcastle -D 'USERINFO' -p < initdb/USERINFO.sql +``` + +## Dependencies + +Required libraries: +- **libmysqlclient-dev** (5.7) - MySQL client library +- **lua5.1-dev** or **luajit** - Lua scripting (used by quest system) +- **xerces-c** (3.2.3) - XML parsing (used by SXml in Core) + +Install on Ubuntu/Debian: +```bash +sudo apt install libxerces-c-dev libmysqlclient-dev liblua5.1-dev +``` + +## Key Game Concepts + +### Races +- **Slayer** - Human vampire hunters +- **Vampire** - Vampire race +- **Ousters** - Another playable race + +### Core Game Systems +- **Zone/ZoneGroup** - Geographic areas where players exist +- **Creature** - Base class for all entities (players, monsters, NPCs) +- **PlayerCreature** - Player-controlled creatures (Slayer, Vampire, Ousters) +- **Effect** - Time-based effects applied to creatures +- **Skill** - Combat and utility abilities +- **Guild/Party** - Social grouping systems +- **DynamicZone** - Instanced content (e.g., dungeons) + +## Running the Servers + +Start servers in this order: +```bash +./bin/loginserver -f ./conf1/loginserver.conf +./bin/sharedserver -f ./conf1/sharedserver.conf +./bin/gameserver -f ./conf1/gameserver.conf +``` + +## Development Notes + +- Source file encoding is **UTF-8** (project was migrated from legacy encodings) +- Use **English** as code comment, there are some legacy Korean or maybe garbled encoding, translate them to English whenever possible +- C++11 standard is used +- Threaded architecture with `ZoneGroupThread` for parallel zone processing +- Extensive use of inheritance (Creature → PlayerCreature → Slayer/Vampire/Ousters) +- Lua scripting is integrated for quest systems (see `quest/luaScript/`) +- Exchange system in `gameserver/exchange/` handles player trading diff --git a/Makefile b/Makefile index 2b866fbe..f8096383 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,9 @@ .PHONY: all fmt fmt fmt-check fmt-check-all clean help debug # Default target -all: +all: debug + +release: cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j$(shell sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4) diff --git a/initdb/DARKEDEN.sql b/initdb/DARKEDEN.sql index 149c0a20..122a4eae 100644 --- a/initdb/DARKEDEN.sql +++ b/initdb/DARKEDEN.sql @@ -11920,6 +11920,99 @@ LOCK TABLES `loginerror` WRITE; /*!40000 ALTER TABLE `loginerror` DISABLE KEYS */; /*!40000 ALTER TABLE `loginerror` ENABLE KEYS */; UNLOCK TABLES; + +-- +-- Table structure for table `ExchangeListing` +-- + +DROP TABLE IF EXISTS `ExchangeListing`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ExchangeListing` ( + `ListingID` BIGINT(20) NOT NULL AUTO_INCREMENT, + `ServerID` SMALLINT(5) UNSIGNED NOT NULL, + `SellerAccount` VARCHAR(50) NOT NULL, + `SellerPlayer` VARCHAR(50) NOT NULL, + `SellerRace` TINYINT(3) UNSIGNED NOT NULL, + `ItemClass` TINYINT(3) UNSIGNED NOT NULL, + `ItemType` SMALLINT(5) UNSIGNED NOT NULL, + `ItemID` BIGINT(20) NOT NULL, + `ObjectID` INT(10) UNSIGNED NOT NULL, + `PricePoint` INT(10) UNSIGNED NOT NULL, + `Currency` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '0=Point Coupon', + `Status` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '0=ACTIVE,1=SOLD,2=CANCELLED,3=EXPIRED', + `BuyerAccount` VARCHAR(50) NULL, + `BuyerPlayer` VARCHAR(50) NULL, + `TaxRate` TINYINT(3) UNSIGNED NOT NULL DEFAULT '8', + `TaxAmount` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `CreatedAt` DATETIME NOT NULL, + `ExpireAt` DATETIME NOT NULL, + `SoldAt` DATETIME NULL, + `CancelledAt` DATETIME NULL, + `UpdatedAt` DATETIME NOT NULL, + `Version` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `ItemName` VARCHAR(100) NULL, + `EnchantLevel` TINYINT(3) UNSIGNED NULL, + `Grade` SMALLINT(5) UNSIGNED NULL, + `Durability` INT(10) UNSIGNED NULL, + `Silver` SMALLINT(5) UNSIGNED NULL, + `OptionType1` TINYINT(3) UNSIGNED NULL, + `OptionType2` TINYINT(3) UNSIGNED NULL, + `OptionType3` TINYINT(3) UNSIGNED NULL, + `OptionValue1` SMALLINT(5) UNSIGNED NULL, + `OptionValue2` SMALLINT(5) UNSIGNED NULL, + `OptionValue3` SMALLINT(5) UNSIGNED NULL, + `StackCount` INT(10) UNSIGNED NULL, + PRIMARY KEY (`ListingID`), + UNIQUE KEY `UNQ_Listings_Item` (`ItemClass`,`ItemID`,`ObjectID`), + KEY `IDX_Listings_Server_Status_CreatedAt` (`ServerID`,`Status`,`CreatedAt`), + KEY `IDX_Listings_Server_ItemClass_ItemType` (`ServerID`,`ItemClass`,`ItemType`), + KEY `IDX_Listings_SellerAccount_Status` (`SellerAccount`,`Status`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ExchangeListing` +-- + +LOCK TABLES `ExchangeListing` WRITE; +/*!40000 ALTER TABLE `ExchangeListing` DISABLE KEYS */; +/*!40000 ALTER TABLE `ExchangeListing` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ExchangeOrder` +-- + +DROP TABLE IF EXISTS `ExchangeOrder`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ExchangeOrder` ( + `OrderID` BIGINT(20) NOT NULL AUTO_INCREMENT, + `ListingID` BIGINT(20) UNIQUE NOT NULL, + `ServerID` SMALLINT(5) UNSIGNED NOT NULL, + `BuyerAccount` VARCHAR(50) NOT NULL, + `BuyerPlayer` VARCHAR(50) NOT NULL, + `PricePoint` INT(10) UNSIGNED NOT NULL, + `TaxAmount` INT(10) UNSIGNED NOT NULL, + `Status` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '0=PAID,1=DELIVERED,2=CANCELLED', + `CreatedAt` DATETIME NOT NULL, + `DeliveredAt` DATETIME NULL, + `CancelledAt` DATETIME NULL, + PRIMARY KEY (`OrderID`), + KEY `IDX_Orders_BuyerPlayer_Status` (`BuyerPlayer`,`Status`), + KEY `IDX_Orders_ListingID` (`ListingID`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ExchangeOrder` +-- + +LOCK TABLES `ExchangeOrder` WRITE; +/*!40000 ALTER TABLE `ExchangeOrder` DISABLE KEYS */; +/*!40000 ALTER TABLE `ExchangeOrder` ENABLE KEYS */; +UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; diff --git a/initdb/USERINFO.sql b/initdb/USERINFO.sql index 107f72c1..777acb1d 100644 --- a/initdb/USERINFO.sql +++ b/initdb/USERINFO.sql @@ -221,6 +221,62 @@ LOCK TABLES `UserStatus` WRITE; INSERT INTO `UserStatus` VALUES (1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(1,0,1); /*!40000 ALTER TABLE `UserStatus` ENABLE KEYS */; UNLOCK TABLES; + +-- +-- Table structure for table `AccountPoint` +-- + +DROP TABLE IF EXISTS `AccountPoint`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8mb4 */; +CREATE TABLE `AccountPoint` ( + `Account` VARCHAR(50) NOT NULL, + `PointBalance` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `UpdatedAt` DATETIME NOT NULL, + PRIMARY KEY (`Account`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `AccountPoint` +-- + +LOCK TABLES `AccountPoint` WRITE; +/*!40000 ALTER TABLE `AccountPoint` DISABLE KEYS */; +/*!40000 ALTER TABLE `AccountPoint` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `PointLedger` +-- + +DROP TABLE IF EXISTS `PointLedger`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8mb4 */; +CREATE TABLE `PointLedger` ( + `LedgerID` BIGINT(20) NOT NULL AUTO_INCREMENT, + `Account` VARCHAR(50) NOT NULL, + `Delta` INT(11) NOT NULL COMMENT 'Positive=increase, Negative=decrease', + `BalanceAfter` INT(10) UNSIGNED NOT NULL, + `Reason` TINYINT(3) UNSIGNED NOT NULL COMMENT '0=BUY,1=SALE,2=TAX,3=REFUND,4=ADJUST', + `RefListingID` BIGINT(20) NULL, + `RefOrderID` BIGINT(20) NULL, + `IdempotencyKey` VARCHAR(64) NULL, + `CreatedAt` DATETIME NOT NULL, + PRIMARY KEY (`LedgerID`), + UNIQUE KEY `UNQ_Ledger_IdempotencyKey` (`IdempotencyKey`), + KEY `IDX_Ledger_Account_CreatedAt` (`Account`,`CreatedAt`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `PointLedger` +-- + +LOCK TABLES `PointLedger` WRITE; +/*!40000 ALTER TABLE `PointLedger` DISABLE KEYS */; +/*!40000 ALTER TABLE `PointLedger` ENABLE KEYS */; +UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; diff --git a/src/Core/CGExchangeBuy.cpp b/src/Core/CGExchangeBuy.cpp new file mode 100644 index 00000000..9304e013 --- /dev/null +++ b/src/Core/CGExchangeBuy.cpp @@ -0,0 +1,56 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : CGExchangeBuy.cpp +// Written By : Exchange System +// Description : Client requests to buy an item +////////////////////////////////////////////////////////////////////////////// + +#include "CGExchangeBuy.h" +#include "GCExchangeBuy.h" + +#include "PlayerCreature.h" +#include "../server/gameserver/exchange/ExchangeService.h" + +void CGExchangeBuy::read(SocketInputStream& iStream) { + __BEGIN_TRY + + uint64_t listingID; + iStream.read(listingID); + m_ListingID = (int64_t)listingID; + + iStream.read(m_IdempotencyKey); + + __END_CATCH +} + +void CGExchangeBuy::write(SocketOutputStream& oStream) const { + __BEGIN_TRY + + oStream.write((uint64_t)m_ListingID); + oStream.write(m_IdempotencyKey); + + __END_CATCH +} + +PacketSize_t CGExchangeBuy::getPacketSize() const { + PacketSize_t size = 0; + size += sizeof(m_ListingID); + size += m_IdempotencyKey.size(); + return size; +} + +string CGExchangeBuy::toString() const { + StringStream msg; + msg << "CGExchangeBuy(" + << "ListingID:" << (int)m_ListingID + << ",IdempotencyKey:" << m_IdempotencyKey + << ")"; + return msg.toString(); +} + +void CGExchangeBuy::execute(Player* pPlayer) { + __BEGIN_TRY + + CGExchangeBuyHandler::execute(this, pPlayer); + + __END_CATCH +} diff --git a/src/Core/CGExchangeBuy.h b/src/Core/CGExchangeBuy.h new file mode 100644 index 00000000..e5a929eb --- /dev/null +++ b/src/Core/CGExchangeBuy.h @@ -0,0 +1,69 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : CGExchangeBuy.h +// Written By : Exchange System +// Description : Client requests to buy an item +////////////////////////////////////////////////////////////////////////////// + +#ifndef __CG_EXCHANGE_BUY_H__ +#define __CG_EXCHANGE_BUY_H__ + +#include "Packet.h" +#include "PacketFactory.h" + +////////////////////////////////////////////////////////////////////////////// +// class CGExchangeBuy +////////////////////////////////////////////////////////////////////////////// + +class CGExchangeBuy : public Packet { +public: + CGExchangeBuy() : m_ListingID(0) {}; + virtual ~CGExchangeBuy() {}; + + void read(SocketInputStream& iStream); + void write(SocketOutputStream& oStream) const; + void execute(Player* pPlayer); + + PacketSize_t getPacketSize() const; + PacketID_t getPacketID() const { return PACKET_CG_EXCHANGE_BUY; } + string getPacketName() const { return "CGExchangeBuy"; } + string toString() const; + + // Getters/Setters + int64_t getListingID() const { return m_ListingID; } + void setListingID(int64_t id) { m_ListingID = id; } + + const string& getIdempotencyKey() const { return m_IdempotencyKey; } + void setIdempotencyKey(const string& key) { m_IdempotencyKey = key; } + +private: + int64_t m_ListingID; + string m_IdempotencyKey; +}; + +////////////////////////////////////////////////////////////////////////////// +// class CGExchangeBuyFactory +////////////////////////////////////////////////////////////////////////////// + +class CGExchangeBuyFactory : public PacketFactory { +public: + Packet* createPacket() { return new CGExchangeBuy(); } + string getPacketName() const { return "CGExchangeBuy"; } + PacketID_t getPacketID() const { return Packet::PACKET_CG_EXCHANGE_BUY; } + PacketSize_t getPacketMaxSize() const { + return sizeof(uint64_t) + // listingID + 64; // idempotencyKey (max 64 chars) + } +}; + +////////////////////////////////////////////////////////////////////////////// +// class CGExchangeBuyHandler +////////////////////////////////////////////////////////////////////////////// + +class GCExchangeBuy; + +class CGExchangeBuyHandler { +public: + static void execute(CGExchangeBuy* pPacket, Player* pPlayer); +}; + +#endif // __CG_EXCHANGE_BUY_H__ diff --git a/src/Core/CGExchangeBuyHandler.cpp b/src/Core/CGExchangeBuyHandler.cpp new file mode 100644 index 00000000..a78cab88 --- /dev/null +++ b/src/Core/CGExchangeBuyHandler.cpp @@ -0,0 +1,41 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : CGExchangeBuyHandler.cpp +// Written By : Exchange System +// Description : Handler for CGExchangeBuy +////////////////////////////////////////////////////////////////////////////// + +#include "CGExchangeBuy.h" +#include "GCExchangeBuy.h" + +#ifdef __GAME_SERVER__ +#include "PlayerCreature.h" +#include "GamePlayer.h" +#endif + +#include "../server/gameserver/exchange/ExchangeService.h" + +void CGExchangeBuyHandler::execute(CGExchangeBuy* pPacket, Player* pPlayer) { + __BEGIN_TRY + + // Validate player + if (pPlayer == NULL) return; + + PlayerCreature* pPC = dynamic_cast(pPlayer); + if (pPC == NULL) return; + + // Call service to buy + pair result = ExchangeService::buyListing( + pPC, + pPacket->getListingID(), + pPacket->getIdempotencyKey() + ); + + // Send response + GCExchangeBuy gcPacket; + gcPacket.setSuccess(result.first); + gcPacket.setMessage(result.second); + + pPlayer->sendPacket(&gcPacket); + + __END_CATCH +} diff --git a/src/Core/CGExchangeList.cpp b/src/Core/CGExchangeList.cpp new file mode 100644 index 00000000..43925387 --- /dev/null +++ b/src/Core/CGExchangeList.cpp @@ -0,0 +1,90 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : CGExchangeList.cpp +// Written By : Exchange System +// Description : Client requests to browse exchange listings +////////////////////////////////////////////////////////////////////////////// + +#include "CGExchangeList.h" +#include "GCExchangeList.h" + +#include "PlayerCreature.h" + +// Forward declaration of exchange service +#include "../server/gameserver/exchange/ExchangeService.h" + +void CGExchangeList::read(SocketInputStream& iStream) { + __BEGIN_TRY + + iStream.read(m_Page); + iStream.read(m_PageSize); + iStream.read(m_ItemClass); + iStream.read(m_ItemType); + iStream.read(m_MinPrice); + iStream.read(m_MaxPrice); + + // Read seller filter string + uint8_t len; + iStream.read(len); + if (len > 0) { + char buf[256]; + iStream.read(buf, len); + buf[len] = '\0'; + m_SellerFilter = string(buf); + } + + __END_CATCH +} + +void CGExchangeList::write(SocketOutputStream& oStream) const { + __BEGIN_TRY + + oStream.write(m_Page); + oStream.write(m_PageSize); + oStream.write(m_ItemClass); + oStream.write(m_ItemType); + oStream.write(m_MinPrice); + oStream.write(m_MaxPrice); + + // Write seller filter string + uint8_t len = (uint8_t)m_SellerFilter.length(); + oStream.write(len); + if (len > 0) oStream.write(m_SellerFilter.c_str(), len); + + __END_CATCH +} + +PacketSize_t CGExchangeList::getPacketSize() const { + PacketSize_t size = 0; + size += sizeof(m_Page); + size += sizeof(m_PageSize); + size += sizeof(m_ItemClass); + size += sizeof(m_ItemType); + size += sizeof(m_MinPrice); + size += sizeof(m_MaxPrice); + size += sizeof(uint8_t); // seller filter length + size += m_SellerFilter.length(); + return size; +} + +string CGExchangeList::toString() const { + StringStream msg; + msg << "CGExchangeList(" + << "Page:" << (int)m_Page + << ",PageSize:" << (int)m_PageSize + << ",ItemClass:" << (int)m_ItemClass + << ",ItemType:" << (int)m_ItemType + << ",MinPrice:" << m_MinPrice + << ",MaxPrice:" << m_MaxPrice + << ",SellerFilter:" << m_SellerFilter + << ")"; + return msg.toString(); +} + +void CGExchangeList::execute(Player* pPlayer) { + __BEGIN_TRY + + CGExchangeListHandler::execute(this, pPlayer); + + __END_CATCH +} + diff --git a/src/Core/CGExchangeList.h b/src/Core/CGExchangeList.h new file mode 100644 index 00000000..9749cf4e --- /dev/null +++ b/src/Core/CGExchangeList.h @@ -0,0 +1,95 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : CGExchangeList.h +// Written By : Exchange System +// Description : Client requests to browse exchange listings +////////////////////////////////////////////////////////////////////////////// + +#ifndef __CG_EXCHANGE_LIST_H__ +#define __CG_EXCHANGE_LIST_H__ + +#include "Packet.h" +#include "PacketFactory.h" +#include "Types.h" + +////////////////////////////////////////////////////////////////////////////// +// class CGExchangeList +////////////////////////////////////////////////////////////////////////////// + +class CGExchangeList : public Packet { +public: + CGExchangeList() {}; + virtual ~CGExchangeList() {}; + + void read(SocketInputStream& iStream); + void write(SocketOutputStream& oStream) const; + void execute(Player* pPlayer); + + PacketSize_t getPacketSize() const; + PacketID_t getPacketID() const { return PACKET_CG_EXCHANGE_LIST; } + string getPacketName() const { return "CGExchangeList"; } + string toString() const; + + // Getters + int getPage() const { return m_Page; } + void setPage(int page) { m_Page = page; } + + int getPageSize() const { return m_PageSize; } + void setPageSize(int size) { m_PageSize = size; } + + uint8_t getItemClass() const { return m_ItemClass; } + void setItemClass(uint8_t itemClass) { m_ItemClass = itemClass; } + + uint16_t getItemType() const { return m_ItemType; } + void setItemType(uint16_t itemType) { m_ItemType = itemType; } + + int getMinPrice() const { return m_MinPrice; } + void setMinPrice(int price) { m_MinPrice = price; } + + int getMaxPrice() const { return m_MaxPrice; } + void setMaxPrice(int price) { m_MaxPrice = price; } + + const string& getSellerFilter() const { return m_SellerFilter; } + void setSellerFilter(const string& filter) { m_SellerFilter = filter; } + +private: + int m_Page; // Page number (1-based) + int m_PageSize; // Items per page + uint8_t m_ItemClass; // Filter by item class (0xFF = all) + uint16_t m_ItemType; // Filter by item type (0xFFFF = all) + int m_MinPrice; // Minimum price filter (0 = no min) + int m_MaxPrice; // Maximum price filter (0 = no max) + string m_SellerFilter; // Filter by seller name +}; + +////////////////////////////////////////////////////////////////////////////// +// class CGExchangeListFactory +////////////////////////////////////////////////////////////////////////////// + +class CGExchangeListFactory : public PacketFactory { +public: + Packet* createPacket() { return new CGExchangeList(); } + string getPacketName() const { return "CGExchangeList"; } + PacketID_t getPacketID() const { return Packet::PACKET_CG_EXCHANGE_LIST; } + PacketSize_t getPacketMaxSize() const { + return szBYTE + // m_Page + szBYTE + // m_PageSize + szBYTE + // m_ItemClass + szWORD + // m_ItemType (uint16) + sizeof(int) + // m_MinPrice + sizeof(int) + // m_MaxPrice + 64; // m_SellerFilter (max 64 chars) + } +}; + +////////////////////////////////////////////////////////////////////////////// +// class CGExchangeListHandler +////////////////////////////////////////////////////////////////////////////// + +class GCExchangeList; + +class CGExchangeListHandler { +public: + static void execute(CGExchangeList* pPacket, Player* pPlayer); +}; + +#endif // __CG_EXCHANGE_LIST_H__ diff --git a/src/Core/CGExchangeListHandler.cpp b/src/Core/CGExchangeListHandler.cpp new file mode 100644 index 00000000..b9f4d6b0 --- /dev/null +++ b/src/Core/CGExchangeListHandler.cpp @@ -0,0 +1,51 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : CGExchangeListHandler.cpp +// Written By : Exchange System +// Description : Handler for CGExchangeList +////////////////////////////////////////////////////////////////////////////// + +#include "CGExchangeList.h" +#include "GCExchangeList.h" + +#ifdef __GAME_SERVER__ +#include "PlayerCreature.h" +#include "GamePlayer.h" +#endif + +// Forward declaration of exchange service +#include "../server/gameserver/exchange/ExchangeService.h" + +void CGExchangeListHandler::execute(CGExchangeList* pPacket, Player* pPlayer) { + __BEGIN_TRY + + // Validate player + if (pPlayer == NULL) return; + + PlayerCreature* pPC = dynamic_cast(pPlayer); + if (pPC == NULL) return; + + // Get server ID (TODO: implement getServerID in PlayerCreature) + int16_t serverID = 1; // Default server ID + + // Get listings from service + vector listings = ExchangeService::getListings( + serverID, + pPacket->getPage(), + pPacket->getPageSize(), + pPacket->getItemClass(), + pPacket->getItemType(), + pPacket->getMinPrice(), + pPacket->getMaxPrice() + ); + + // Send response + GCExchangeList gcPacket; + gcPacket.setListings(listings); + gcPacket.setPage(pPacket->getPage()); + gcPacket.setPageSize(pPacket->getPageSize()); + gcPacket.setTotal(listings.size()); + + pPlayer->sendPacket(&gcPacket); + + __END_CATCH +} diff --git a/src/Core/CGNPCTalkHandler.cpp b/src/Core/CGNPCTalkHandler.cpp index f6778634..6922bdfd 100644 --- a/src/Core/CGNPCTalkHandler.cpp +++ b/src/Core/CGNPCTalkHandler.cpp @@ -8,6 +8,7 @@ #ifdef __GAME_SERVER__ #include "GCNPCResponse.h" +#include "GCNPCAsk.h" #include "GCNPCSayDynamic.h" #include "GQuestManager.h" #include "GamePlayer.h" @@ -87,6 +88,28 @@ void CGNPCTalkHandler::execute(CGNPCTalk* pPacket, Player* pPlayer) return; } + // Exchange System: Check if this is an Exchange Broker NPC + // NPC ID for exchange broker (can be configured in database) + const int EXCHANGE_NPC_ID = 10001; // TODO: Make configurable + if (pNPC->getNPCID() == EXCHANGE_NPC_ID) { + // Send exchange menu to player + // For now, just send a basic response + // Client will handle opening the exchange UI + GCNPCResponse gcNPCResponse; + pPlayer->sendPacket(&gcNPCResponse); + + // Send NPC ask packet with exchange menu options + // Client should recognize this NPC ID and open exchange UI + GCNPCAsk askPacket; + askPacket.setObjectID(pNPC->getObjectID()); + askPacket.setNPCID(pNPC->getNPCID()); + // Set script ID for exchange broker menu + askPacket.setScriptID(5001); // Exchange Broker Script ID + pPlayer->sendPacket(&askPacket); + + return; + } + // 퀘스트로 만난 넘일때 ^^; if (pPC->getGQuestManager()->metNPC(pNPC)) { // 일단 클라이언트를 위해 OK패킷을 함 날린다. diff --git a/src/Core/CMakeLists.txt b/src/Core/CMakeLists.txt index 0e5cf1e7..7d5c52f6 100755 --- a/src/Core/CMakeLists.txt +++ b/src/Core/CMakeLists.txt @@ -352,6 +352,9 @@ set(GC_PACKET_SOURCES GCWarScheduleList.cpp GCWarScheduleListHandler.cpp GCWhisper.cpp GCWhisperHandler.cpp GCWhisperFailed.cpp GCWhisperFailedHandler.cpp + # Exchange System Packets + GCExchangeList.cpp GCExchangeListHandler.cpp + GCExchangeBuy.cpp GCExchangeBuyHandler.cpp ) @@ -505,6 +508,9 @@ set(CG_PACKET_SOURCES CGWhisper.cpp CGWhisperHandler.cpp CGWithdrawPet.cpp CGWithdrawPetHandler.cpp CGWithdrawTax.cpp CGWithdrawTaxHandler.cpp + # Exchange System Packets + CGExchangeList.cpp CGExchangeListHandler.cpp + CGExchangeBuy.cpp CGExchangeBuyHandler.cpp ) # CL Packets (Client -> Login) diff --git a/src/Core/GCExchangeBuy.cpp b/src/Core/GCExchangeBuy.cpp new file mode 100644 index 00000000..3e1612a9 --- /dev/null +++ b/src/Core/GCExchangeBuy.cpp @@ -0,0 +1,58 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : GCExchangeBuy.cpp +// Written By : Exchange System +// Description : Server responds to buy request +////////////////////////////////////////////////////////////////////////////// + +#include "GCExchangeBuy.h" + +// Stub execute() for server side (GC packets don't execute on server) +void GCExchangeBuy::execute(Player* pPlayer) { + __BEGIN_TRY + // Server doesn't execute GC packets + __END_CATCH +} + +void GCExchangeBuy::read(SocketInputStream& iStream) { + __BEGIN_TRY + + uint8_t success; + iStream.read(success); + m_Success = (success != 0); + + iStream.read(m_Message); + + uint64_t orderID; + iStream.read(orderID); + m_OrderID = (int64_t)orderID; + + __END_CATCH +} + +void GCExchangeBuy::write(SocketOutputStream& oStream) const { + __BEGIN_TRY + + oStream.write((uint8_t)m_Success); + oStream.write(m_Message); + oStream.write((uint64_t)m_OrderID); + + __END_CATCH +} + +PacketSize_t GCExchangeBuy::getPacketSize() const { + PacketSize_t size = 0; + size += sizeof(uint8_t); + size += m_Message.size(); + size += sizeof(m_OrderID); + return size; +} + +string GCExchangeBuy::toString() const { + StringStream msg; + msg << "GCExchangeBuy(" + << "Success:" << (m_Success ? "true" : "false") + << ",Message:" << m_Message + << ",OrderID:" << (int)m_OrderID + << ")"; + return msg.toString(); +} diff --git a/src/Core/GCExchangeBuy.h b/src/Core/GCExchangeBuy.h new file mode 100644 index 00000000..e553470c --- /dev/null +++ b/src/Core/GCExchangeBuy.h @@ -0,0 +1,59 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : GCExchangeBuy.h +// Written By : Exchange System +// Description : Server responds to buy request +////////////////////////////////////////////////////////////////////////////// + +#ifndef __GC_EXCHANGE_BUY_H__ +#define __GC_EXCHANGE_BUY_H__ + +#include "Packet.h" + +#include + +using namespace std; + +////////////////////////////////////////////////////////////////////////////// +// class GCExchangeBuy +////////////////////////////////////////////////////////////////////////////// + +class GCExchangeBuy : public Packet { +public: + GCExchangeBuy() : m_Success(false), m_OrderID(0) {}; + virtual ~GCExchangeBuy() {}; + + void read(SocketInputStream& iStream); + void write(SocketOutputStream& oStream) const; + void execute(Player* pPlayer); // Stub for server side + + PacketSize_t getPacketSize() const; + PacketID_t getPacketID() const { return PACKET_GC_EXCHANGE_BUY; } + string getPacketName() const { return "GCExchangeBuy"; } + string toString() const; + + // Setters + void setSuccess(bool success) { m_Success = success; } + void setMessage(const string& message) { m_Message = message; } + void setOrderID(int64_t orderID) { m_OrderID = orderID; } + + // Getters + bool getSuccess() const { return m_Success; } + const string& getMessage() const { return m_Message; } + int64_t getOrderID() const { return m_OrderID; } + +private: + bool m_Success; + string m_Message; + int64_t m_OrderID; +}; + +////////////////////////////////////////////////////////////////////////////// +// class GCExchangeBuyHandler +////////////////////////////////////////////////////////////////////////////// + +class GCExchangeBuyHandler { +public: + static void execute(GCExchangeBuy* pPacket, Player* pPlayer); +}; + +#endif // __GC_EXCHANGE_BUY_H__ diff --git a/src/Core/GCExchangeBuyHandler.cpp b/src/Core/GCExchangeBuyHandler.cpp new file mode 100644 index 00000000..863d8be4 --- /dev/null +++ b/src/Core/GCExchangeBuyHandler.cpp @@ -0,0 +1,19 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : GCExchangeBuyHandler.cpp +// Written By : Exchange System +// Description : Handler for GCExchangeBuy +////////////////////////////////////////////////////////////////////////////// + +#include "GCExchangeBuy.h" + +void GCExchangeBuyHandler::execute(GCExchangeBuy* pPacket, Player* pPlayer) { + __BEGIN_TRY + __BEGIN_DEBUG_EX + +#ifdef __GAME_CLIENT__ + // Client-side handling would be here +#endif + + __END_DEBUG_EX + __END_CATCH +} diff --git a/src/Core/GCExchangeList.cpp b/src/Core/GCExchangeList.cpp new file mode 100644 index 00000000..36dee9b3 --- /dev/null +++ b/src/Core/GCExchangeList.cpp @@ -0,0 +1,273 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : GCExchangeList.cpp +// Written By : Exchange System +// Description : Server sends listing list to client +////////////////////////////////////////////////////////////////////////////// + +#include "GCExchangeList.h" + +#include + +using namespace std; + +// Stub execute() for server side (GC packets don't execute on server) +void GCExchangeList::execute(Player* pPlayer) { + __BEGIN_TRY + // Server doesn't execute GC packets + __END_CATCH +} + +void GCExchangeList::read(SocketInputStream& iStream) { + __BEGIN_TRY + + // Read page info + iStream.read(m_Page); + iStream.read(m_PageSize); + iStream.read(m_Total); + + // Read listing count + uint16_t count; + iStream.read(count); + + // Read listings + for (uint16_t i = 0; i < count; i++) { + ExchangeListing listing; + + int64_t listingID; + uint16_t serverID; + uint8_t sellerRace, itemClass, currency, status, taxRate; + uint16_t itemType; + int pricePoint, taxAmount, objectID, version; + + iStream.read(listingID); + iStream.read(serverID); + + // Strings need special handling + char buf[256]; + uint8_t len; + + // SellerAccount + iStream.read(len); + iStream.read(buf, len); + buf[len] = '\0'; + listing.sellerAccount = string(buf); + + // SellerPlayer + iStream.read(len); + iStream.read(buf, len); + buf[len] = '\0'; + listing.sellerPlayer = string(buf); + + iStream.read(sellerRace); + iStream.read(itemClass); + iStream.read(itemType); + iStream.read(listingID); // itemID + iStream.read(objectID); + iStream.read(pricePoint); + iStream.read(currency); + iStream.read(status); + + // BuyerAccount + iStream.read(len); + if (len > 0) { + iStream.read(buf, len); + buf[len] = '\0'; + listing.buyerAccount = string(buf); + } + + // BuyerPlayer + iStream.read(len); + if (len > 0) { + iStream.read(buf, len); + buf[len] = '\0'; + listing.buyerPlayer = string(buf); + } + + iStream.read(taxRate); + iStream.read(taxAmount); + + // Timestamp strings + iStream.read(len); + if (len > 0) { + iStream.read(buf, len); + buf[len] = '\0'; + listing.createdAt = string(buf); + } + + iStream.read(len); + if (len > 0) { + iStream.read(buf, len); + buf[len] = '\0'; + listing.expireAt = string(buf); + } + + iStream.read(version); + + // Snapshot fields + // ItemName + iStream.read(len); + if (len > 0) { + iStream.read(buf, len); + buf[len] = '\0'; + listing.itemName = string(buf); + } + + iStream.read(listing.enchantLevel); + iStream.read(listing.grade); + iStream.read(listing.durability); + iStream.read(listing.silver); + iStream.read(listing.optionType1); + iStream.read(listing.optionType2); + iStream.read(listing.optionType3); + iStream.read(listing.optionValue1); + iStream.read(listing.optionValue2); + iStream.read(listing.optionValue3); + iStream.read(listing.stackCount); + + m_Listings.push_back(listing); + } + + __END_CATCH +} + +void GCExchangeList::write(SocketOutputStream& oStream) const { + __BEGIN_TRY + + // Write page info + oStream.write(m_Page); + oStream.write(m_PageSize); + oStream.write(m_Total); + + // Write listing count + uint16_t count = (uint16_t)m_Listings.size(); + oStream.write(count); + + // Write listings + for (const auto& listing : m_Listings) { + // Write basic fields + oStream.write((uint64_t)listing.listingID); + oStream.write((uint16_t)listing.serverID); + + // Write strings with length prefix + // SellerAccount + uint8_t len = (uint8_t)listing.sellerAccount.length(); + oStream.write(len); + if (len > 0) oStream.write(listing.sellerAccount.c_str(), len); + + // SellerPlayer + len = (uint8_t)listing.sellerPlayer.length(); + oStream.write(len); + if (len > 0) oStream.write(listing.sellerPlayer.c_str(), len); + + oStream.write(listing.sellerRace); + oStream.write(listing.itemClass); + oStream.write(listing.itemType); + oStream.write((uint64_t)listing.itemID); + oStream.write(listing.objectID); + oStream.write(listing.pricePoint); + oStream.write(listing.currency); + oStream.write(listing.status); + + // BuyerAccount + len = (uint8_t)listing.buyerAccount.length(); + oStream.write(len); + if (len > 0) oStream.write(listing.buyerAccount.c_str(), len); + + // BuyerPlayer + len = (uint8_t)listing.buyerPlayer.length(); + oStream.write(len); + if (len > 0) oStream.write(listing.buyerPlayer.c_str(), len); + + oStream.write(listing.taxRate); + oStream.write(listing.taxAmount); + + // Timestamp strings + len = (uint8_t)listing.createdAt.length(); + oStream.write(len); + if (len > 0) oStream.write(listing.createdAt.c_str(), len); + + len = (uint8_t)listing.expireAt.length(); + oStream.write(len); + if (len > 0) oStream.write(listing.expireAt.c_str(), len); + + oStream.write(listing.version); + + // Snapshot fields + len = (uint8_t)listing.itemName.length(); + oStream.write(len); + if (len > 0) oStream.write(listing.itemName.c_str(), len); + + oStream.write(listing.enchantLevel); + oStream.write(listing.grade); + oStream.write(listing.durability); + oStream.write(listing.silver); + oStream.write(listing.optionType1); + oStream.write(listing.optionType2); + oStream.write(listing.optionType3); + oStream.write(listing.optionValue1); + oStream.write(listing.optionValue2); + oStream.write(listing.optionValue3); + oStream.write(listing.stackCount); + } + + __END_CATCH +} + +PacketSize_t GCExchangeList::getPacketSize() const { + PacketSize_t size = 0; + size += sizeof(m_Page); + size += sizeof(m_PageSize); + size += sizeof(m_Total); + size += sizeof(uint16_t); // count + + for (const auto& listing : m_Listings) { + size += sizeof(uint64_t); // listingID + size += sizeof(uint16_t); // serverID + size += sizeof(uint8_t) + listing.sellerAccount.length(); + size += sizeof(uint8_t) + listing.sellerPlayer.length(); + size += sizeof(listing.sellerRace); + size += sizeof(listing.itemClass); + size += sizeof(listing.itemType); + size += sizeof(uint64_t); // itemID + size += sizeof(listing.objectID); + size += sizeof(listing.pricePoint); + size += sizeof(listing.currency); + size += sizeof(listing.status); + size += sizeof(uint8_t) + listing.buyerAccount.length(); + size += sizeof(uint8_t) + listing.buyerPlayer.length(); + size += sizeof(listing.taxRate); + size += sizeof(listing.taxAmount); + size += sizeof(uint8_t) + listing.createdAt.length(); + size += sizeof(uint8_t) + listing.expireAt.length(); + size += sizeof(listing.version); + size += sizeof(uint8_t) + listing.itemName.length(); + size += sizeof(listing.enchantLevel); + size += sizeof(listing.grade); + size += sizeof(listing.durability); + size += sizeof(listing.silver); + size += sizeof(listing.optionType1); + size += sizeof(listing.optionType2); + size += sizeof(listing.optionType3); + size += sizeof(listing.optionValue1); + size += sizeof(listing.optionValue2); + size += sizeof(listing.optionValue3); + size += sizeof(listing.stackCount); + } + + return size; +} + +string GCExchangeList::toString() const { + StringStream msg; + msg << "GCExchangeList(" + << "Page:" << m_Page + << ",PageSize:" << m_PageSize + << ",Total:" << m_Total + << ",Count:" << m_Listings.size() + << ")"; + return msg.toString(); +} + +void GCExchangeList::setListings(const vector& listings) { + m_Listings = listings; +} diff --git a/src/Core/GCExchangeList.h b/src/Core/GCExchangeList.h new file mode 100644 index 00000000..5a331fb8 --- /dev/null +++ b/src/Core/GCExchangeList.h @@ -0,0 +1,105 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : GCExchangeList.h +// Written By : Exchange System +// Description : Server sends listing list to client +////////////////////////////////////////////////////////////////////////////// + +#ifndef __GC_EXCHANGE_LIST_H__ +#define __GC_EXCHANGE_LIST_H__ + +#include "Packet.h" +#include +#include + +using namespace std; + +////////////////////////////////////////////////////////////////////////////// +// ExchangeListing structure (partial, for packet transfer) +////////////////////////////////////////////////////////////////////////////// + +struct ExchangeListing { + int64_t listingID; + int16_t serverID; + string sellerAccount; + string sellerPlayer; + uint8_t sellerRace; + uint8_t itemClass; + uint16_t itemType; + int64_t itemID; + int objectID; + int pricePoint; + uint8_t currency; + uint8_t status; + string buyerAccount; + string buyerPlayer; + uint8_t taxRate; + int taxAmount; + string createdAt; + string expireAt; + string soldAt; + string cancelledAt; + string updatedAt; + int version; + + // Snapshot fields for UI display + string itemName; + uint8_t enchantLevel; + uint16_t grade; + int durability; + uint16_t silver; + uint8_t optionType1; + uint8_t optionType2; + uint8_t optionType3; + uint16_t optionValue1; + uint16_t optionValue2; + uint16_t optionValue3; + int stackCount; +}; + +////////////////////////////////////////////////////////////////////////////// +// class GCExchangeList +////////////////////////////////////////////////////////////////////////////// + +class GCExchangeList : public Packet { +public: + GCExchangeList() : m_Page(1), m_PageSize(20), m_Total(0) {}; + virtual ~GCExchangeList() {}; + + void read(SocketInputStream& iStream); + void write(SocketOutputStream& oStream) const; + void execute(Player* pPlayer); // Stub for server side + + PacketSize_t getPacketSize() const; + PacketID_t getPacketID() const { return PACKET_GC_EXCHANGE_LIST; } + string getPacketName() const { return "GCExchangeList"; } + string toString() const; + + // Setters + void setListings(const vector& listings); + void setPage(int page) { m_Page = page; } + void setPageSize(int pageSize) { m_PageSize = pageSize; } + void setTotal(int total) { m_Total = total; } + + // Getters + const vector& getListings() const { return m_Listings; } + int getPage() const { return m_Page; } + int getPageSize() const { return m_PageSize; } + int getTotal() const { return m_Total; } + +private: + vector m_Listings; + int m_Page; + int m_PageSize; + int m_Total; +}; + +////////////////////////////////////////////////////////////////////////////// +// class GCExchangeListHandler +////////////////////////////////////////////////////////////////////////////// + +class GCExchangeListHandler { +public: + static void execute(GCExchangeList* pPacket, Player* pPlayer); +}; + +#endif // __GC_EXCHANGE_LIST_H__ diff --git a/src/Core/GCExchangeListHandler.cpp b/src/Core/GCExchangeListHandler.cpp new file mode 100644 index 00000000..3017630a --- /dev/null +++ b/src/Core/GCExchangeListHandler.cpp @@ -0,0 +1,19 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : GCExchangeListHandler.cpp +// Written By : Exchange System +// Description : Handler for GCExchangeList +////////////////////////////////////////////////////////////////////////////// + +#include "GCExchangeList.h" + +void GCExchangeListHandler::execute(GCExchangeList* pPacket, Player* pPlayer) { + __BEGIN_TRY + __BEGIN_DEBUG_EX + +#ifdef __GAME_CLIENT__ + // Client-side handling would be here +#endif + + __END_DEBUG_EX + __END_CATCH +} diff --git a/src/Core/Packet.h b/src/Core/Packet.h index 8fb54c79..17e708ec 100644 --- a/src/Core/Packet.h +++ b/src/Core/Packet.h @@ -550,6 +550,26 @@ class Packet { PACKET_UC_UPDATE_LIST, // 482 PACKET_CG_ENCODE_KEY, // 483 add by viva 2008-12-27 : Packet Encode Key PACKET_GC_FRIEND_CHATTING, // add by viva + + // Exchange System Packets + PACKET_CG_EXCHANGE_LIST, // 484 - Get listing list + PACKET_CG_EXCHANGE_CREATE_LISTING, // 485 - Create new listing + PACKET_CG_EXCHANGE_CANCEL_LISTING, // 486 - Cancel listing + PACKET_CG_EXCHANGE_BUY, // 487 - Buy item + PACKET_CG_EXCHANGE_CLAIM, // 488 - Claim item + PACKET_CG_EXCHANGE_MY_LISTINGS, // 489 - Get my listings + PACKET_CG_EXCHANGE_ORDERS, // 490 - Get my orders + PACKET_CG_EXCHANGE_BALANCE, // 491 - Get point balance + PACKET_GC_EXCHANGE_LIST, // 492 - Listing list response + PACKET_GC_EXCHANGE_CREATE_LISTING, // 493 - Create listing response + PACKET_GC_EXCHANGE_CANCEL_LISTING, // 494 - Cancel listing response + PACKET_GC_EXCHANGE_BUY, // 495 - Buy response + PACKET_GC_EXCHANGE_CLAIM_LIST, // 496 - Claim list response + PACKET_GC_EXCHANGE_CLAIM_RESULT, // 497 - Claim result + PACKET_GC_EXCHANGE_MY_LISTINGS, // 498 - My listings response + PACKET_GC_EXCHANGE_ORDERS, // 499 - My orders response + PACKET_GC_EXCHANGE_BALANCE, // 500 - Balance response + PACKET_MAX }; diff --git a/src/Core/PacketFactoryManager.cpp b/src/Core/PacketFactoryManager.cpp index 11c33664..55a1f12e 100644 --- a/src/Core/PacketFactoryManager.cpp +++ b/src/Core/PacketFactoryManager.cpp @@ -431,6 +431,12 @@ #include "CGDonationMoney.h" #include "CGDownSkill.h" #include "CGExpelGuild.h" + +// Exchange System Packets +#include "CGExchangeList.h" +#include "CGExchangeBuy.h" +#include "GCExchangeList.h" +#include "GCExchangeBuy.h" #include "CGFailQuest.h" #include "CGGQuestAccept.h" #include "CGGQuestCancel.h" @@ -1163,6 +1169,12 @@ void PacketFactoryManager::init() { addFactory(new SGGuildMemberLogOnOKFactory()); #endif + // Exchange System Packets +#if defined(__GAME_SERVER__) + addFactory(new CGExchangeListFactory()); + addFactory(new CGExchangeBuyFactory()); +#endif + #if __OUTPUT_INIT__ cout << toString() << endl; diff --git a/src/Core/types/ItemTypes.h b/src/Core/types/ItemTypes.h index ef44f5f4..67d900e5 100644 --- a/src/Core/types/ItemTypes.h +++ b/src/Core/types/ItemTypes.h @@ -68,7 +68,8 @@ enum Storage { STORAGE_GARBAGE, // 10 STORAGE_TIMEOVER, // 11 STORAGE_GOODSINVENTORY, // 12 - STORAGE_PET_STASH // 13 // pet 보관함 + STORAGE_PET_STASH, // 13 // pet 보관함 + STORAGE_EXCHANGE // 14 // exchange warehouse }; typedef BYTE Storage_t; diff --git a/src/server/gameserver/CMakeLists.txt b/src/server/gameserver/CMakeLists.txt index e6f9afec..d27c003e 100755 --- a/src/server/gameserver/CMakeLists.txt +++ b/src/server/gameserver/CMakeLists.txt @@ -381,6 +381,14 @@ set(GAMESERVER_MAIN_SOURCES NewYear2005ItemUtil.cpp ) +# ============================================================================ +# Exchange System 相关源文件 +# ============================================================================ +set(EXCHANGE_SOURCES + exchange/ExchangeService.cpp + exchange/ExchangeDB.cpp +) + # ============================================================================ # GameServer 可执行文件 # ============================================================================ @@ -392,6 +400,7 @@ add_executable(gameserver ${EFFECT_SOURCES} ${EVENT_SOURCES} ${GQUEST_SOURCES} + ${EXCHANGE_SOURCES} ) # 编译定义 @@ -432,3 +441,24 @@ target_link_libraries(gameserver PRIVATE # 导出符号(用于动态加载) set_target_properties(gameserver PROPERTIES ENABLE_EXPORTS ON) + +# ============================================================================ +# Exchange System Test Program +# ============================================================================ +# Temporarily disabled - test_exchange.cpp is broken +# add_executable(test_exchange +# exchange/test_exchange.cpp +# exchange/ExchangeDB.cpp +# ${EXTRA_SOURCES} +# ) +# +# target_compile_definitions(test_exchange PRIVATE ${GAMESERVER_COMPILE_DEFS}) +# target_include_directories(test_exchange PRIVATE ${GAMESERVER_INCLUDE_DIRS}) +# +# target_link_libraries(test_exchange PRIVATE +# GameServerDatabase +# ServerCore +# Core +# ${MYSQL_LIBRARY} +# ${EXTRA_LIBS} +# ) diff --git a/src/server/gameserver/ClientManager.cpp b/src/server/gameserver/ClientManager.cpp index 1be1db9d..86a2ee26 100644 --- a/src/server/gameserver/ClientManager.cpp +++ b/src/server/gameserver/ClientManager.cpp @@ -133,7 +133,7 @@ void ClientManager::run() // Sleep 1ms to reduce CPU usage. try { // I/O Multiplexing - usleep(100); + usleep(1000); // FIX: 注释说 1ms,但代码写的是 0.1ms,已修正 // g_pIncomingPlayerManager->copyPlayers(); // game global time setting diff --git a/src/server/gameserver/CreatureUtil.cpp b/src/server/gameserver/CreatureUtil.cpp index 32904704..aa31db06 100644 --- a/src/server/gameserver/CreatureUtil.cpp +++ b/src/server/gameserver/CreatureUtil.cpp @@ -39,6 +39,7 @@ #include "GamePlayer.h" #include "GameWorldInfoManager.h" #include "Inventory.h" +#include "ItemUtil.h" #include "LevelWarManager.h" #include "Monster.h" #include "MonsterInfo.h" @@ -1460,6 +1461,10 @@ bool canDropToZone(Creature* pCreature, Item* pItem) { break; } + // Exchange System: Point-only items cannot be dropped + if (isPointOnlyTradeItem(pItem)) + return false; + return true; } diff --git a/src/server/gameserver/EffectOnBridge.cpp b/src/server/gameserver/EffectOnBridge.cpp index 3314ddc7..474d0d72 100644 --- a/src/server/gameserver/EffectOnBridge.cpp +++ b/src/server/gameserver/EffectOnBridge.cpp @@ -111,9 +111,12 @@ void EffectOnBridgeLoader::load(Zone* pZone) if (tile.canAddEffect()) { EffectOnBridge* pEffect = new EffectOnBridge(pZone, X, Y); - // 존 및 타일에다가 이펙트를 추가한다. + // Tile-level effects should NOT be added to Zone's EffectManager. + // They are permanent (deadline=99999999) and managed by Tile itself. + // Adding them to Zone's EffectManager causes severe CPU overhead + // because heartbeat() iterates through all effects every tick. pZone->registerObject(pEffect); - pZone->addEffect(pEffect); + // pZone->addEffect(pEffect); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect(pEffect); } } diff --git a/src/server/gameserver/ItemUtil.cpp b/src/server/gameserver/ItemUtil.cpp index 33d7c6c5..92817d99 100644 --- a/src/server/gameserver/ItemUtil.cpp +++ b/src/server/gameserver/ItemUtil.cpp @@ -7,6 +7,12 @@ #include "ItemUtil.h" #include +#include + +// Enable Exchange System functions +#ifndef __THAILAND_SERVER__ +#define __THAILAND_SERVER__ +#endif #include @@ -2478,6 +2484,10 @@ bool canTrade(Item* pItem) { if (itemClass == Item::ITEM_CLASS_MIXING_ITEM && itemType == 18) return false; + // Exchange System: Point-only items cannot be P2P traded + if (isPointOnlyTradeItem(pItem)) + return false; + return true; } bool isCoupleRing(Item* pItem) { @@ -2983,4 +2993,75 @@ ItemType_t getItemTypeByItemLimit(Item::ItemClass itemClass, ItemType_t itemType return rItemType; } +////////////////////////////////////////////////////////////////////////////// +// Exchange System: Point-only trade item check functions +////////////////////////////////////////////////////////////////////////////// + +// Check if item is Blue Sapphire (hard currency) +// Blue Sapphire: ItemClass = EVENT_STAR, ItemType = 6 +bool isBlueSapphire(Item* pItem) { + if (pItem == NULL) return false; + + Item::ItemClass itemClass = pItem->getItemClass(); + ItemType_t itemType = pItem->getItemType(); + + return (itemClass == Item::ITEM_CLASS_EVENT_STAR && itemType == 6); +} + +// Get base option type by following PreviousType chain +// Returns the root (most basic) option type in the upgrade chain +OptionType_t getBaseOptionType(OptionType_t type) { + OptionType_t cur = type; + unordered_set seen; // Prevent infinite loops from circular references + + while (cur != 0) { + // Check for circular reference + if (seen.count(cur)) break; + seen.insert(cur); + + OptionInfo* pInfo = g_pOptionInfoManager->getOptionInfo(cur); + if (pInfo == NULL) break; // Missing option info - stop here + + OptionType_t prev = pInfo->getPreviousType(); + if (prev == 0 || prev == cur) break; // Reached root or self-reference + cur = prev; + } + + return cur; +} + +// Check if item has 3 options and at least one is upgraded +// Upgraded means: current option type != base option type +bool isUpgradedThreeOptionItem(Item* pItem) { + if (pItem == NULL) return false; + + // Must have exactly 3 options + if (pItem->getOptionTypeSize() != 3) return false; + + const list& optionTypes = pItem->getOptionTypeList(); + + // Check if any option is upgraded + for (OptionType_t t : optionTypes) { + OptionType_t baseType = getBaseOptionType(t); + if (baseType != t) { + return true; // At least one option is upgraded + } + } + + return false; +} + +// Check if item can ONLY be traded via exchange (using points) +// This includes: +// 1. Blue Sapphire (hard currency) +// 2. Three-option items with at least one upgraded option +bool isPointOnlyTradeItem(Item* pItem) { + if (pItem == NULL) return false; + + if (isBlueSapphire(pItem)) return true; + if (isUpgradedThreeOptionItem(pItem)) return true; + + return false; +} + #endif // __THAILAND_SERVER__ diff --git a/src/server/gameserver/ItemUtil.h b/src/server/gameserver/ItemUtil.h index 9b6e2a27..3e562259 100644 --- a/src/server/gameserver/ItemUtil.h +++ b/src/server/gameserver/ItemUtil.h @@ -301,6 +301,21 @@ bool canPutInStash(Item* pItem); bool canTrade(Item* pItem); bool isCoupleRing(Item* pItem); +////////////////////////////////////////////////////////////////////////////// +// Exchange System: Point-only trade item check functions +////////////////////////////////////////////////////////////////////////////// +// Check if item is Blue Sapphire (hard currency) +bool isBlueSapphire(Item* pItem); + +// Get base option type by following PreviousType chain +OptionType_t getBaseOptionType(OptionType_t type); + +// Check if item has 3 options and at least one is upgraded +bool isUpgradedThreeOptionItem(Item* pItem); + +// Check if item can ONLY be traded via exchange (points) +bool isPointOnlyTradeItem(Item* pItem); + bool suitableItemClass(Item::ItemClass iClass, SkillDomainType_t domainType); // 아이템을 성별에 맞는 동급의 아이템으로 바꿔준다. 아이템 타입이 남자용 다음에 바로 같은 급의 여자용이 온다고 가정 diff --git a/src/server/gameserver/LoginServerManager.cpp b/src/server/gameserver/LoginServerManager.cpp index b034416c..c2ee8b0d 100644 --- a/src/server/gameserver/LoginServerManager.cpp +++ b/src/server/gameserver/LoginServerManager.cpp @@ -98,7 +98,7 @@ void LoginServerManager::run() { getCurrentTime(dummyQueryTime); while (true) { - usleep(100); + usleep(1000); // FIX: 降低 CPU 占用率,从 100 微秒改为 1000 微秒(1ms) Datagram* pDatagram = NULL; DatagramPacket* pDatagramPacket = NULL; @@ -166,7 +166,7 @@ void LoginServerManager::run() { filelog("LOGINSERVERMANAGER.log", "LoginServerManager::run() 3 : %s", t.toString().c_str()); } - usleep(100); + usleep(1000); // FIX: 降低 CPU 占用率 Timeval currentTime; getCurrentTime(currentTime); diff --git a/src/server/gameserver/SharedServerManager.cpp b/src/server/gameserver/SharedServerManager.cpp index 132d6c51..94c4f740 100644 --- a/src/server/gameserver/SharedServerManager.cpp +++ b/src/server/gameserver/SharedServerManager.cpp @@ -87,7 +87,7 @@ void SharedServerManager::run() getCurrentTime(dummyQueryTime); while (true) { - usleep(100); + usleep(1000); // FIX: 降低 CPU 占用率 // 연결되어 있지 않다면 연결을 시도한다. if (m_pSharedServerClient == NULL) { diff --git a/src/server/gameserver/Store.cpp b/src/server/gameserver/Store.cpp index 0c13a72f..374f5111 100644 --- a/src/server/gameserver/Store.cpp +++ b/src/server/gameserver/Store.cpp @@ -1,6 +1,7 @@ #include "Store.h" #include "Item.h" +#include "ItemUtil.h" BYTE StoreItem::removeItem() { if (!m_bExist) @@ -131,3 +132,14 @@ void Store::updateStoreInfo() { m_StoreItems[index].makeStoreItemInfo(m_StoreInfo.getStoreItemInfo(index)); } } + +// Exchange System: Check if item can be added to store +bool Store::canAddItem(Item* pItem) const { + if (pItem == NULL) return false; + + // Point-only items cannot be sold in player stores + if (isPointOnlyTradeItem(pItem)) + return false; + + return true; +} diff --git a/src/server/gameserver/Store.h b/src/server/gameserver/Store.h index 3b2a600e..ae85aa36 100644 --- a/src/server/gameserver/Store.h +++ b/src/server/gameserver/Store.h @@ -85,6 +85,9 @@ class Store { void updateStoreInfo(); + // Exchange System: Check if item can be added to store + bool canAddItem(Item* pItem) const; + private: bool m_bOpen; string m_Sign; diff --git a/src/server/gameserver/Zone.cpp b/src/server/gameserver/Zone.cpp index 2b7638df..afb196f6 100644 --- a/src/server/gameserver/Zone.cpp +++ b/src/server/gameserver/Zone.cpp @@ -7808,6 +7808,10 @@ void Zone::heartbeat() m_pLockedEffectManager->heartbeat(currentTime); __LEAVE_CRITICAL_SECTION(m_MutexEffect) + // Debug: 统计 zone 的 effects + static time_t lastZoneLogTime = 0; + size_t zoneEffects = m_pEffectManager->getSize(); + m_pEffectManager->heartbeat(currentTime); endProfileEx("Z_EFFECT"); @@ -7821,6 +7825,7 @@ void Zone::heartbeat() // item heartbeaet int i = 0; + size_t totalItemEffects = 0; for (unordered_map::iterator itr = m_Items.begin(); itr != m_Items.end(); itr++) { Item* pItem = itr->second; @@ -7830,6 +7835,7 @@ void Zone::heartbeat() m_LastItemClass = (int)pItem->getItemClass(); EffectManager& rEffectManager = pItem->getEffectManager(); + totalItemEffects += rEffectManager.getSize(); rEffectManager.heartbeat(currentTime); i++; } diff --git a/src/server/gameserver/ZoneGroupThread.cpp b/src/server/gameserver/ZoneGroupThread.cpp index a7148079..dc80b40c 100644 --- a/src/server/gameserver/ZoneGroupThread.cpp +++ b/src/server/gameserver/ZoneGroupThread.cpp @@ -123,7 +123,7 @@ void ZoneGroupThread::run() try { beginProfileExNoTry("ZGT_MAIN"); - usleep(100); // CPU 점유율을 줄이기 위해서 강제로 0.001 초동안 쉰다. + usleep(1000); // FIX: 原注释说 0.001秒 = 1000微秒,但代码写的是 100 微秒,已修正 __ENTER_CRITICAL_SECTION((*m_pZoneGroup)) diff --git a/src/server/gameserver/exchange/ExchangeDB.cpp b/src/server/gameserver/exchange/ExchangeDB.cpp new file mode 100644 index 00000000..02762ce6 --- /dev/null +++ b/src/server/gameserver/exchange/ExchangeDB.cpp @@ -0,0 +1,934 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : ExchangeDB.cpp +// Written by : Exchange System +// Description : Database access layer implementation for Exchange System +////////////////////////////////////////////////////////////////////////////// + +#include "ExchangeDB.h" +#include "GCExchangeList.h" // For ExchangeListing definition + +#include +#include +#include + +#include "DatabaseManager.h" +#include "DB.h" +#include "StringStream.h" + +////////////////////////////////////////////////////////////////////////////// +// Helper functions +////////////////////////////////////////////////////////////////////////////// + +namespace { + +// Get current timestamp in MySQL format +string getCurrentTime() { + time_t now = time(NULL); + struct tm* t = localtime(&now); + char buffer[20]; + snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d", + t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, + t->tm_hour, t->tm_min, t->tm_sec); + return string(buffer); +} + +// Escape string for SQL +string escapeSQL(const string& input) { + string result = input; + size_t pos = 0; + while ((pos = result.find("'", pos)) != string::npos) { + result.replace(pos, 1, "''"); + pos += 2; + } + return result; +} + +// Convert string to int64_t +int64_t toInt64(const string& str) { + return strtoll(str.c_str(), NULL, 10); +} + +} // anonymous namespace + +////////////////////////////////////////////////////////////////////////////// +// Listing operations +////////////////////////////////////////////////////////////////////////////// + +// Create a new listing record +int64_t ExchangeDB::createListing(const ExchangeListing& listing) { + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + // Build INSERT query - using printf-style format + pStmt->executeQuery( + "INSERT INTO ExchangeListing (" + "ServerID, SellerAccount, SellerPlayer, SellerRace, " + "ItemClass, ItemType, ItemID, ObjectID, " + "PricePoint, Currency, Status, " + "TaxRate, TaxAmount, " + "CreatedAt, ExpireAt, UpdatedAt, Version, " + "ItemName, EnchantLevel, Grade, Durability, Silver, " + "OptionType1, OptionType2, OptionType3, " + "OptionValue1, OptionValue2, OptionValue3, StackCount" + ") VALUES (%d, '%s', '%s', %d, %d, %d, %lld, %d, %d, %d, %d, %d, %d, '%s', '%s', '%s', %d, '%s', %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)", + listing.serverID, + escapeSQL(listing.sellerAccount).c_str(), + escapeSQL(listing.sellerPlayer).c_str(), + (int)listing.sellerRace, + (int)listing.itemClass, + listing.itemType, + (long long)listing.itemID, + listing.objectID, + listing.pricePoint, + (int)listing.currency, + (int)listing.status, + (int)listing.taxRate, + listing.taxAmount, + getCurrentTime().c_str(), + listing.expireAt.c_str(), + getCurrentTime().c_str(), + listing.version, + escapeSQL(listing.itemName).c_str(), + (int)listing.enchantLevel, + listing.grade, + listing.durability, + listing.silver, + (int)listing.optionType1, + (int)listing.optionType2, + (int)listing.optionType3, + listing.optionValue1, + listing.optionValue2, + listing.optionValue3, + listing.stackCount + ); + + // Get the auto-generated listing ID + pResult = pStmt->executeQuery("SELECT LAST_INSERT_ID()"); + if (pResult->next()) { + int64_t listingID = toInt64(pResult->getString(1)); + SAFE_DELETE(pStmt); + return listingID; + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + return 0; + + __END_CATCH +} + +// Cancel a listing (mark as CANCELLED) +bool ExchangeDB::cancelListing(int64_t listingID) { + __BEGIN_TRY + + Statement* pStmt = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pStmt->executeQuery( + "UPDATE ExchangeListing SET " + "Status = 2, " // CANCELLED + "CancelledAt = '%s', " + "UpdatedAt = '%s' " + "WHERE ListingID = %lld " + "AND Status = 0", // Only ACTIVE listings + getCurrentTime().c_str(), + getCurrentTime().c_str(), + (long long)listingID + ); + + SAFE_DELETE(pStmt); + return true; + } END_DB(pStmt) + + __END_CATCH + + return false; +} + +// Mark listing as expired +bool ExchangeDB::expireListing(int64_t listingID) { + __BEGIN_TRY + + Statement* pStmt = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pStmt->executeQuery( + "UPDATE ExchangeListing SET " + "Status = 3, " // EXPIRED + "UpdatedAt = '%s' " + "WHERE ListingID = %lld " + "AND Status = 0", // Only ACTIVE listings + getCurrentTime().c_str(), + (long long)listingID + ); + + SAFE_DELETE(pStmt); + return true; + } END_DB(pStmt) + + __END_CATCH + + return false; +} + +// Mark listing as sold and set buyer info +bool ExchangeDB::markListingSold(int64_t listingID, + const string& buyerAccount, + const string& buyerPlayer) { + __BEGIN_TRY + + Statement* pStmt = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pStmt->executeQuery( + "UPDATE ExchangeListing SET " + "Status = 1, " // SOLD + "BuyerAccount = '%s', " + "BuyerPlayer = '%s', " + "SoldAt = '%s', " + "UpdatedAt = '%s' " + "WHERE ListingID = %lld " + "AND Status = 0", // Only ACTIVE listings + escapeSQL(buyerAccount).c_str(), + escapeSQL(buyerPlayer).c_str(), + getCurrentTime().c_str(), + getCurrentTime().c_str(), + (long long)listingID + ); + + SAFE_DELETE(pStmt); + return true; + } END_DB(pStmt) + + __END_CATCH + + return false; +} + +// Query listings with pagination and filters +vector ExchangeDB::getListings(int16_t serverID, + uint8_t status, + int page, + int pageSize) { + vector listings; + + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + int offset = (page - 1) * pageSize; + + pResult = pStmt->executeQuery( + "SELECT * FROM ExchangeListing " + "WHERE ServerID = %d " + "AND Status = %d " + "ORDER BY CreatedAt DESC " + "LIMIT %d OFFSET %d", + serverID, + (int)status, + pageSize, + offset + ); + + while (pResult->next()) { + ExchangeListing listing; + int idx = 1; + + listing.listingID = toInt64(pResult->getString(idx++)); + listing.serverID = pResult->getInt(idx++); + listing.sellerAccount = pResult->getString(idx++); + listing.sellerPlayer = pResult->getString(idx++); + listing.sellerRace = pResult->getBYTE(idx++); + listing.itemClass = pResult->getBYTE(idx++); + listing.itemType = pResult->getWORD(idx++); + listing.itemID = toInt64(pResult->getString(idx++)); + listing.objectID = pResult->getInt(idx++); + listing.pricePoint = pResult->getInt(idx++); + listing.currency = pResult->getBYTE(idx++); + listing.status = pResult->getBYTE(idx++); + listing.buyerAccount = pResult->getString(idx++); + listing.buyerPlayer = pResult->getString(idx++); + listing.taxRate = pResult->getBYTE(idx++); + listing.taxAmount = pResult->getInt(idx++); + listing.createdAt = pResult->getString(idx++); + listing.expireAt = pResult->getString(idx++); + listing.soldAt = pResult->getString(idx++); + listing.cancelledAt = pResult->getString(idx++); + listing.updatedAt = pResult->getString(idx++); + listing.version = pResult->getInt(idx++); + + // Snapshot fields + listing.itemName = pResult->getString(idx++); + listing.enchantLevel = pResult->getBYTE(idx++); + listing.grade = pResult->getWORD(idx++); + listing.durability = pResult->getInt(idx++); + listing.silver = pResult->getWORD(idx++); + listing.optionType1 = pResult->getBYTE(idx++); + listing.optionType2 = pResult->getBYTE(idx++); + listing.optionType3 = pResult->getBYTE(idx++); + listing.optionValue1 = pResult->getWORD(idx++); + listing.optionValue2 = pResult->getWORD(idx++); + listing.optionValue3 = pResult->getWORD(idx++); + listing.stackCount = pResult->getInt(idx++); + + listings.push_back(listing); + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + __END_CATCH + + return listings; +} + +// Get a specific listing by ID +ExchangeListing* ExchangeDB::getListing(int64_t listingID) { + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pResult = pStmt->executeQuery( + "SELECT * FROM ExchangeListing " + "WHERE ListingID = %lld", + (long long)listingID + ); + + if (pResult->next()) { + ExchangeListing* listing = new ExchangeListing(); + int idx = 1; + + listing->listingID = toInt64(pResult->getString(idx++)); + listing->serverID = pResult->getInt(idx++); + listing->sellerAccount = pResult->getString(idx++); + listing->sellerPlayer = pResult->getString(idx++); + listing->sellerRace = pResult->getBYTE(idx++); + listing->itemClass = pResult->getBYTE(idx++); + listing->itemType = pResult->getWORD(idx++); + listing->itemID = toInt64(pResult->getString(idx++)); + listing->objectID = pResult->getInt(idx++); + listing->pricePoint = pResult->getInt(idx++); + listing->currency = pResult->getBYTE(idx++); + listing->status = pResult->getBYTE(idx++); + listing->buyerAccount = pResult->getString(idx++); + listing->buyerPlayer = pResult->getString(idx++); + listing->taxRate = pResult->getBYTE(idx++); + listing->taxAmount = pResult->getInt(idx++); + listing->createdAt = pResult->getString(idx++); + listing->expireAt = pResult->getString(idx++); + listing->soldAt = pResult->getString(idx++); + listing->cancelledAt = pResult->getString(idx++); + listing->updatedAt = pResult->getString(idx++); + listing->version = pResult->getInt(idx++); + + // Snapshot fields + listing->itemName = pResult->getString(idx++); + listing->enchantLevel = pResult->getBYTE(idx++); + listing->grade = pResult->getWORD(idx++); + listing->durability = pResult->getInt(idx++); + listing->silver = pResult->getWORD(idx++); + listing->optionType1 = pResult->getBYTE(idx++); + listing->optionType2 = pResult->getBYTE(idx++); + listing->optionType3 = pResult->getBYTE(idx++); + listing->optionValue1 = pResult->getWORD(idx++); + listing->optionValue2 = pResult->getWORD(idx++); + listing->optionValue3 = pResult->getWORD(idx++); + listing->stackCount = pResult->getInt(idx++); + + SAFE_DELETE(pStmt); + return listing; + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + __END_CATCH + + return NULL; +} + +// Get seller's listings +vector ExchangeDB::getSellerListings(const string& sellerAccount, + uint8_t status) { + vector listings; + + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pResult = pStmt->executeQuery( + "SELECT * FROM ExchangeListing " + "WHERE SellerAccount = '%s' " + "AND Status = %d " + "ORDER BY CreatedAt DESC", + escapeSQL(sellerAccount).c_str(), + (int)status + ); + + while (pResult->next()) { + ExchangeListing listing; + int idx = 1; + + listing.listingID = toInt64(pResult->getString(idx++)); + listing.serverID = pResult->getInt(idx++); + listing.sellerAccount = pResult->getString(idx++); + listing.sellerPlayer = pResult->getString(idx++); + listing.sellerRace = pResult->getBYTE(idx++); + listing.itemClass = pResult->getBYTE(idx++); + listing.itemType = pResult->getWORD(idx++); + listing.itemID = toInt64(pResult->getString(idx++)); + listing.objectID = pResult->getInt(idx++); + listing.pricePoint = pResult->getInt(idx++); + listing.currency = pResult->getBYTE(idx++); + listing.status = pResult->getBYTE(idx++); + listing.buyerAccount = pResult->getString(idx++); + listing.buyerPlayer = pResult->getString(idx++); + listing.taxRate = pResult->getBYTE(idx++); + listing.taxAmount = pResult->getInt(idx++); + listing.createdAt = pResult->getString(idx++); + listing.expireAt = pResult->getString(idx++); + listing.soldAt = pResult->getString(idx++); + listing.cancelledAt = pResult->getString(idx++); + listing.updatedAt = pResult->getString(idx++); + listing.version = pResult->getInt(idx++); + + // Snapshot fields + listing.itemName = pResult->getString(idx++); + listing.enchantLevel = pResult->getBYTE(idx++); + listing.grade = pResult->getWORD(idx++); + listing.durability = pResult->getInt(idx++); + listing.silver = pResult->getWORD(idx++); + listing.optionType1 = pResult->getBYTE(idx++); + listing.optionType2 = pResult->getBYTE(idx++); + listing.optionType3 = pResult->getBYTE(idx++); + listing.optionValue1 = pResult->getWORD(idx++); + listing.optionValue2 = pResult->getWORD(idx++); + listing.optionValue3 = pResult->getWORD(idx++); + listing.stackCount = pResult->getInt(idx++); + + listings.push_back(listing); + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + __END_CATCH + + return listings; +} + +// Get expired listings for maintenance scan +vector ExchangeDB::getExpiredListings() { + vector listings; + + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + // Query active listings that have expired + // Use NOW() to compare with ExpireAt + pResult = pStmt->executeQuery( + "SELECT * FROM ExchangeListing " + "WHERE Status = %d " // LISTING_STATUS_ACTIVE = 0 + "AND ExpireAt < NOW() " + "ORDER BY ExpireAt ASC " + "LIMIT 1000", // Process in batches to avoid long transactions + (int)LISTING_STATUS_ACTIVE + ); + + while (pResult->next()) { + ExchangeListing listing; + int idx = 1; + + listing.listingID = toInt64(pResult->getString(idx++)); + listing.serverID = pResult->getInt(idx++); + listing.sellerAccount = pResult->getString(idx++); + listing.sellerPlayer = pResult->getString(idx++); + listing.sellerRace = pResult->getBYTE(idx++); + listing.itemClass = pResult->getBYTE(idx++); + listing.itemType = pResult->getWORD(idx++); + listing.itemID = toInt64(pResult->getString(idx++)); + listing.objectID = pResult->getInt(idx++); + listing.pricePoint = pResult->getInt(idx++); + listing.currency = pResult->getBYTE(idx++); + listing.status = pResult->getBYTE(idx++); + listing.buyerAccount = pResult->getString(idx++); + listing.buyerPlayer = pResult->getString(idx++); + listing.taxRate = pResult->getBYTE(idx++); + listing.taxAmount = pResult->getInt(idx++); + listing.createdAt = pResult->getString(idx++); + listing.expireAt = pResult->getString(idx++); + listing.soldAt = pResult->getString(idx++); + listing.cancelledAt = pResult->getString(idx++); + listing.updatedAt = pResult->getString(idx++); + listing.version = pResult->getInt(idx++); + + // Snapshot fields + listing.itemName = pResult->getString(idx++); + listing.enchantLevel = pResult->getBYTE(idx++); + listing.grade = pResult->getWORD(idx++); + listing.durability = pResult->getInt(idx++); + listing.silver = pResult->getWORD(idx++); + listing.optionType1 = pResult->getBYTE(idx++); + listing.optionType2 = pResult->getBYTE(idx++); + listing.optionType3 = pResult->getBYTE(idx++); + listing.optionValue1 = pResult->getWORD(idx++); + listing.optionValue2 = pResult->getWORD(idx++); + listing.optionValue3 = pResult->getWORD(idx++); + listing.stackCount = pResult->getInt(idx++); + + listings.push_back(listing); + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + __END_CATCH + + return listings; +} + +////////////////////////////////////////////////////////////////////////////// +// Order operations +////////////////////////////////////////////////////////////////////////////// + +// Create a new order +int64_t ExchangeDB::createOrder(const ExchangeOrder& order) { + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pStmt->executeQuery( + "INSERT INTO ExchangeOrder (" + "ListingID, ServerID, BuyerAccount, BuyerPlayer, " + "PricePoint, TaxAmount, Status, CreatedAt" + ") VALUES (%lld, %d, '%s', '%s', %d, %d, %d, '%s')", + (long long)order.listingID, + order.serverID, + escapeSQL(order.buyerAccount).c_str(), + escapeSQL(order.buyerPlayer).c_str(), + order.pricePoint, + order.taxAmount, + (int)order.status, + getCurrentTime().c_str() + ); + + // Get the auto-generated order ID + pResult = pStmt->executeQuery("SELECT LAST_INSERT_ID()"); + if (pResult->next()) { + int64_t orderID = toInt64(pResult->getString(1)); + SAFE_DELETE(pStmt); + return orderID; + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + return 0; + + __END_CATCH +} + +// Mark order as delivered +bool ExchangeDB::markOrderDelivered(int64_t orderID) { + __BEGIN_TRY + + Statement* pStmt = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pStmt->executeQuery( + "UPDATE ExchangeOrder SET " + "Status = 1, " // DELIVERED + "DeliveredAt = '%s' " + "WHERE OrderID = %lld " + "AND Status = 0", // Only PAID orders + getCurrentTime().c_str(), + (long long)orderID + ); + + SAFE_DELETE(pStmt); + return true; + } END_DB(pStmt) + + __END_CATCH + + return false; +} + +// Get buyer's orders +vector ExchangeDB::getBuyerOrders(const string& buyerPlayer, + uint8_t status) { + vector orders; + + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pResult = pStmt->executeQuery( + "SELECT * FROM ExchangeOrder " + "WHERE BuyerPlayer = '%s' " + "AND Status = %d " + "ORDER BY CreatedAt DESC", + escapeSQL(buyerPlayer).c_str(), + (int)status + ); + + while (pResult->next()) { + ExchangeOrder order; + int idx = 1; + + order.orderID = toInt64(pResult->getString(idx++)); + order.listingID = toInt64(pResult->getString(idx++)); + order.serverID = pResult->getInt(idx++); + order.buyerAccount = pResult->getString(idx++); + order.buyerPlayer = pResult->getString(idx++); + order.pricePoint = pResult->getInt(idx++); + order.taxAmount = pResult->getInt(idx++); + order.status = pResult->getBYTE(idx++); + order.createdAt = pResult->getString(idx++); + order.deliveredAt = pResult->getString(idx++); + order.cancelledAt = pResult->getString(idx++); + + orders.push_back(order); + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + __END_CATCH + + return orders; +} + +// Get seller's fulfilled orders +vector ExchangeDB::getSellerOrders(const string& sellerPlayer, + uint8_t status) { + vector orders; + + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + + pResult = pStmt->executeQuery( + "SELECT o.* FROM ExchangeOrder o " + "INNER JOIN ExchangeListing l ON o.ListingID = l.ListingID " + "WHERE l.SellerPlayer = '%s' " + "AND o.Status = %d " + "ORDER BY o.CreatedAt DESC", + escapeSQL(sellerPlayer).c_str(), + (int)status + ); + + while (pResult->next()) { + ExchangeOrder order; + int idx = 1; + + order.orderID = toInt64(pResult->getString(idx++)); + order.listingID = toInt64(pResult->getString(idx++)); + order.serverID = pResult->getInt(idx++); + order.buyerAccount = pResult->getString(idx++); + order.buyerPlayer = pResult->getString(idx++); + order.pricePoint = pResult->getInt(idx++); + order.taxAmount = pResult->getInt(idx++); + order.status = pResult->getBYTE(idx++); + order.createdAt = pResult->getString(idx++); + order.deliveredAt = pResult->getString(idx++); + order.cancelledAt = pResult->getString(idx++); + + orders.push_back(order); + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + __END_CATCH + + return orders; +} + +////////////////////////////////////////////////////////////////////////////// +// Point operations (cross-database to USERINFO) +////////////////////////////////////////////////////////////////////////////// + +// Adjust point balance with ledger record +bool ExchangeDB::adjustPoints(const string& account, + int delta, + int& balanceAfter, + uint8_t reason, + int64_t refListingID, + int64_t refOrderID, + const string& idempotencyKey) { + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + Connection* pConn = g_pDatabaseManager->getConnection("USERINFO"); + + // Check idempotency if key provided + if (!idempotencyKey.empty()) { + pStmt = pConn->createStatement(); + pResult = pStmt->executeQuery( + "SELECT COUNT(*) FROM PointLedger WHERE IdempotencyKey = '%s'", + escapeSQL(idempotencyKey).c_str() + ); + + if (pResult->next() && pResult->getInt(1) > 0) { + // Idempotency key exists - already processed + SAFE_DELETE(pStmt); + return false; + } + SAFE_DELETE(pStmt); + } + + // Get current balance + pStmt = pConn->createStatement(); + pResult = pStmt->executeQuery( + "SELECT PointBalance FROM AccountPoint WHERE Account = '%s'", + escapeSQL(account).c_str() + ); + + int currentBalance = 0; + if (pResult->next()) { + currentBalance = pResult->getInt(1); + } + SAFE_DELETE(pStmt); + + // Calculate new balance + int newBalance = currentBalance + delta; + if (newBalance < 0) { + // Insufficient balance + return false; + } + + // Update or insert balance using REPLACE + pStmt = pConn->createStatement(); + pStmt->executeQuery( + "REPLACE INTO AccountPoint (Account, PointBalance, UpdatedAt) " + "VALUES ('%s', %d, '%s')", + escapeSQL(account).c_str(), + newBalance, + getCurrentTime().c_str() + ); + SAFE_DELETE(pStmt); + + // Insert ledger record + pStmt = pConn->createStatement(); + if (idempotencyKey.empty()) { + pStmt->executeQuery( + "INSERT INTO PointLedger " + "(Account, Delta, BalanceAfter, Reason, RefListingID, RefOrderID, CreatedAt) " + "VALUES ('%s', %d, %d, %d, %lld, %lld, '%s')", + escapeSQL(account).c_str(), + delta, + newBalance, + (int)reason, + (long long)refListingID, + (long long)refOrderID, + getCurrentTime().c_str() + ); + } else { + pStmt->executeQuery( + "INSERT INTO PointLedger " + "(Account, Delta, BalanceAfter, Reason, RefListingID, RefOrderID, IdempotencyKey, CreatedAt) " + "VALUES ('%s', %d, %d, %d, %lld, %lld, '%s', '%s')", + escapeSQL(account).c_str(), + delta, + newBalance, + (int)reason, + (long long)refListingID, + (long long)refOrderID, + escapeSQL(idempotencyKey).c_str(), + getCurrentTime().c_str() + ); + } + SAFE_DELETE(pStmt); + + balanceAfter = newBalance; + return true; + } END_DB(pStmt) + + __END_CATCH + + return false; +} + +// Get current point balance +int ExchangeDB::getPointBalance(const string& account) { + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("USERINFO")->createStatement(); + + pResult = pStmt->executeQuery( + "SELECT PointBalance FROM AccountPoint WHERE Account = '%s'", + escapeSQL(account).c_str() + ); + + if (pResult->next()) { + int balance = pResult->getInt(1); + SAFE_DELETE(pStmt); + return balance; + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + __END_CATCH + + return 0; +} + +// Check if idempotency key exists +bool ExchangeDB::hasIdempotencyKey(const string& idempotencyKey) { + __BEGIN_TRY + + Statement* pStmt = NULL; + Result* pResult = NULL; + + BEGIN_DB { + pStmt = g_pDatabaseManager->getConnection("USERINFO")->createStatement(); + + pResult = pStmt->executeQuery( + "SELECT COUNT(*) FROM PointLedger WHERE IdempotencyKey = '%s'", + escapeSQL(idempotencyKey).c_str() + ); + + if (pResult->next()) { + int count = pResult->getInt(1); + SAFE_DELETE(pStmt); + return (count > 0); + } + + SAFE_DELETE(pStmt); + } END_DB(pStmt) + + __END_CATCH + + return false; +} + +////////////////////////////////////////////////////////////////////////////// +// Transaction helpers (cross-database) +////////////////////////////////////////////////////////////////////////////// + +// Begin cross-database transaction +bool ExchangeDB::beginTransaction() { + __BEGIN_TRY + + Statement* pStmt = NULL; + + BEGIN_DB { + // Start transaction on DARKEDEN database + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + pStmt->executeQuery("START TRANSACTION"); + SAFE_DELETE(pStmt); + + // Start transaction on USERINFO database + pStmt = g_pDatabaseManager->getConnection("USERINFO")->createStatement(); + pStmt->executeQuery("START TRANSACTION"); + SAFE_DELETE(pStmt); + + return true; + } END_DB(pStmt) + + __END_CATCH + + return false; +} + +// Commit transaction +bool ExchangeDB::commit() { + __BEGIN_TRY + + Statement* pStmt = NULL; + + BEGIN_DB { + // Commit DARKEDEN database + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + pStmt->executeQuery("COMMIT"); + SAFE_DELETE(pStmt); + + // Commit USERINFO database + pStmt = g_pDatabaseManager->getConnection("USERINFO")->createStatement(); + pStmt->executeQuery("COMMIT"); + SAFE_DELETE(pStmt); + + return true; + } END_DB(pStmt) + + __END_CATCH + + return false; +} + +// Rollback transaction +bool ExchangeDB::rollback() { + __BEGIN_TRY + + Statement* pStmt = NULL; + + BEGIN_DB { + // Rollback DARKEDEN database + pStmt = g_pDatabaseManager->getConnection("DARKEDEN")->createStatement(); + pStmt->executeQuery("ROLLBACK"); + SAFE_DELETE(pStmt); + + // Rollback USERINFO database + pStmt = g_pDatabaseManager->getConnection("USERINFO")->createStatement(); + pStmt->executeQuery("ROLLBACK"); + SAFE_DELETE(pStmt); + + return true; + } END_DB(pStmt) + + __END_CATCH + + return false; +} diff --git a/src/server/gameserver/exchange/ExchangeDB.h b/src/server/gameserver/exchange/ExchangeDB.h new file mode 100644 index 00000000..dfcd755a --- /dev/null +++ b/src/server/gameserver/exchange/ExchangeDB.h @@ -0,0 +1,148 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : ExchangeDB.h +// Written by : Exchange System +// Description : Database access layer for Exchange System +////////////////////////////////////////////////////////////////////////////// + +#ifndef __EXCHANGE_DB_H__ +#define __EXCHANGE_DB_H__ + +#include +#include + +#include "Types.h" + +using namespace std; + +////////////////////////////////////////////////////////////////////////////// +// Data structures for Exchange +////////////////////////////////////////////////////////////////////////////// + +// Listing status constants +const uint8_t LISTING_STATUS_ACTIVE = 0; +const uint8_t LISTING_STATUS_SOLD = 1; +const uint8_t LISTING_STATUS_CANCELLED = 2; +const uint8_t LISTING_STATUS_EXPIRED = 3; + +// Order status constants +const uint8_t ORDER_STATUS_PAID = 0; +const uint8_t ORDER_STATUS_DELIVERED = 1; +const uint8_t ORDER_STATUS_CANCELLED = 2; + +// Point transaction reason codes +const uint8_t POINT_REASON_BUY = 0; +const uint8_t POINT_REASON_SALE = 1; +const uint8_t POINT_REASON_TAX = 2; +const uint8_t POINT_REASON_REFUND = 3; +const uint8_t POINT_REASON_ADJUST = 4; + +// Forward declaration (defined in GCExchangeList.h) +struct ExchangeListing; + +struct ExchangeOrder { + int64_t orderID; + int64_t listingID; + int16_t serverID; + string buyerAccount; + string buyerPlayer; + int pricePoint; + int taxAmount; + uint8_t status; + string createdAt; + string deliveredAt; + string cancelledAt; +}; + +////////////////////////////////////////////////////////////////////////////// +// Exchange Database Access Layer +////////////////////////////////////////////////////////////////////////////// + +class ExchangeDB { +public: + ////////////////////////////////////////////////////////////////////////////// + // Listing operations + ////////////////////////////////////////////////////////////////////////////// + + // Create a new listing record + static int64_t createListing(const ExchangeListing& listing); + + // Cancel a listing (mark as CANCELLED) + static bool cancelListing(int64_t listingID); + + // Mark listing as expired + static bool expireListing(int64_t listingID); + + // Mark listing as sold and set buyer info + static bool markListingSold(int64_t listingID, + const string& buyerAccount, + const string& buyerPlayer); + + // Query listings with pagination and filters + static vector getListings(int16_t serverID, + uint8_t status, + int page, + int pageSize); + + // Get a specific listing by ID + static ExchangeListing* getListing(int64_t listingID); + + // Get seller's active listings + static vector getSellerListings(const string& sellerAccount, + uint8_t status); + + // Get expired listings (for maintenance scan) + static vector getExpiredListings(); + + ////////////////////////////////////////////////////////////////////////////// + // Order operations + ////////////////////////////////////////////////////////////////////////////// + + // Create a new order + static int64_t createOrder(const ExchangeOrder& order); + + // Mark order as delivered + static bool markOrderDelivered(int64_t orderID); + + // Get buyer's orders + static vector getBuyerOrders(const string& buyerPlayer, + uint8_t status); + + // Get seller's fulfilled orders + static vector getSellerOrders(const string& sellerPlayer, + uint8_t status); + + ////////////////////////////////////////////////////////////////////////////// + // Point operations (cross-database to USERINFO) + ////////////////////////////////////////////////////////////////////////////// + + // Adjust point balance with ledger record + // Returns true on success, balanceAfter contains new balance + static bool adjustPoints(const string& account, + int delta, + int& balanceAfter, + uint8_t reason, + int64_t refListingID, + int64_t refOrderID, + const string& idempotencyKey); + + // Get current point balance + static int getPointBalance(const string& account); + + // Check if idempotency key exists + static bool hasIdempotencyKey(const string& idempotencyKey); + + ////////////////////////////////////////////////////////////////////////////// + // Transaction helpers (cross-database) + ////////////////////////////////////////////////////////////////////////////// + + // Begin cross-database transaction + static bool beginTransaction(); + + // Commit transaction + static bool commit(); + + // Rollback transaction + static bool rollback(); +}; + +#endif // __EXCHANGE_DB_H__ diff --git a/src/server/gameserver/exchange/ExchangeService.cpp b/src/server/gameserver/exchange/ExchangeService.cpp new file mode 100644 index 00000000..a98a468a --- /dev/null +++ b/src/server/gameserver/exchange/ExchangeService.cpp @@ -0,0 +1,753 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : ExchangeService.cpp +// Written by : Exchange System +// Description : Core business logic service for Exchange System +////////////////////////////////////////////////////////////////////////////// + +#include "ExchangeService.h" +#include "GCExchangeList.h" // For ExchangeListing definition + +#include "Item.h" +#include "ItemUtil.h" +#include "PlayerCreature.h" +#include "Inventory.h" + +#include +#include +#include + +using namespace std; + +////////////////////////////////////////////////////////////////////////////// +// Static members +////////////////////////////////////////////////////////////////////////////// + +uint8_t ExchangeService::m_TaxRate = 8; // Default 8% tax +int ExchangeService::m_ListingDurationDays = 3; // Default 3 days + +////////////////////////////////////////////////////////////////////////////// +// Helper functions +////////////////////////////////////////////////////////////////////////////// + +namespace { + string _getCurrentTime() { + time_t now = time(NULL); + struct tm* t = localtime(&now); + char buf[64]; + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", t); + return string(buf); + } + + string _addHoursToNow(int hours) { + time_t now = time(NULL); + now += hours * 3600; + struct tm* t = localtime(&now); + char buf[64]; + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", t); + return string(buf); + } + + string _toInt64String(int64_t value) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)value); + return string(buf); + } + + // Generate unique idempotency key + string _generateIdempotencyKey() { + static long counter = 0; + time_t now = time(NULL); + char buf[128]; + snprintf(buf, sizeof(buf), "EX_%ld_%ld", (long)now, (long)counter++); + return string(buf); + } + + int16_t _getServerID() { + // TODO: Get actual server ID from configuration + // For now, return a default value + return 1; + } + + // Check if player has inventory space + bool _checkInventorySpace(PlayerCreature* pPlayer) { + if (!pPlayer) return false; + + Inventory* pInv = pPlayer->getInventory(); + if (!pInv) return false; + + // Check if inventory has at least one empty slot + // This is a simplified check - should be more thorough + return pInv->getItemNum() < (pInv->getWidth() * pInv->getHeight()); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Browse operations +////////////////////////////////////////////////////////////////////////////// + +vector ExchangeService::getListings( + int16_t serverID, + int page, + int pageSize, + uint8_t itemClass, + uint16_t itemType, + int minPrice, + int maxPrice, + const string& sellerFilter +) { + // For now, use the basic DB function + // In production, we'd want to add filtering at DB level for performance + vector allListings = ExchangeDB::getListings( + serverID, + LISTING_STATUS_ACTIVE, + page, + pageSize + ); + + // Apply additional filters (in-memory for now) + vector filtered; + for (const auto& listing : allListings) { + if (itemClass != 0xFF && listing.itemClass != itemClass) continue; + if (itemType != 0xFFFF && listing.itemType != itemType) continue; + if (minPrice > 0 && listing.pricePoint < minPrice) continue; + if (maxPrice > 0 && listing.pricePoint > maxPrice) continue; + if (!sellerFilter.empty() && listing.sellerPlayer.find(sellerFilter) == string::npos) continue; + + filtered.push_back(listing); + } + + return filtered; +} + +int ExchangeService::getListingsCount( + int16_t serverID, + uint8_t itemClass, + uint16_t itemType, + int minPrice, + int maxPrice, + const string& sellerFilter +) { + // For accurate count, we need a DB query + // For now, return a placeholder + vector listings = getListings( + serverID, 1, 1000, // Get max results + itemClass, itemType, minPrice, maxPrice, sellerFilter + ); + return listings.size(); +} + +ExchangeListing* ExchangeService::getListing(int64_t listingID) { + return ExchangeDB::getListing(listingID); +} + +////////////////////////////////////////////////////////////////////////////// +// Listing operations +////////////////////////////////////////////////////////////////////////////// + +pair ExchangeService::createListing( + PlayerCreature* pSeller, + Item* pItem, + int pricePoint, + int durationHours +) { + // Validate inputs + if (!pSeller) { + return make_pair(false, formatError(EXCHANGE_FAIL_ITEM_NOT_FOUND)); + } + if (!pItem) { + return make_pair(false, formatError(EXCHANGE_FAIL_ITEM_NOT_FOUND)); + } + if (pricePoint < 1) { + return make_pair(false, formatError(EXCHANGE_FAIL_INVALID_PRICE)); + } + + // Get player info + string account = pSeller->getName(); // Using name as account identifier + string playerName = pSeller->getName(); + uint8_t race = 0; // TODO: Get actual race from PlayerCreature + + // Verify item ownership + Inventory* pInv = pSeller->getInventory(); + if (!pInv || !pInv->hasItem(pItem->getObjectID())) { + return make_pair(false, formatError(EXCHANGE_FAIL_ITEM_OWNERSHIP)); + } + + // Check if item is tradeable + if (!canTrade(pItem)) { + return make_pair(false, formatError(EXCHANGE_FAIL_ITEM_TRADEABLE)); + } + + // Check exchange storage has space (conceptually - items stored by DB) + // In this implementation, items remain in DB with STORAGE_EXCHANGE flag + + // Calculate expiration + string createdAt = _getCurrentTime(); + string expireAt = _addHoursToNow(durationHours); + + // Create listing record + ExchangeListing listing; + listing.listingID = 0; // Will be set by DB auto-increment + listing.serverID = _getServerID(); + listing.sellerAccount = account; + listing.sellerPlayer = playerName; + listing.sellerRace = race; + listing.itemClass = pItem->getItemClass(); + listing.itemType = pItem->getItemType(); + listing.itemID = 0; // TODO: Get ItemID from Item + listing.objectID = pItem->getObjectID(); + listing.pricePoint = pricePoint; + listing.currency = 0; // 0 = points + listing.status = LISTING_STATUS_ACTIVE; + listing.buyerAccount = ""; + listing.buyerPlayer = ""; + listing.taxRate = m_TaxRate; + listing.taxAmount = 0; + listing.createdAt = createdAt; + listing.expireAt = expireAt; + listing.soldAt = ""; + listing.cancelledAt = ""; + listing.updatedAt = createdAt; + listing.version = 0; + + // Create item snapshot + createItemSnapshot(pItem, listing); + + // Save to database + int64_t listingID = ExchangeDB::createListing(listing); + if (listingID <= 0) { + return make_pair(false, formatError(EXCHANGE_FAIL_DATABASE_ERROR)); + } + + // Move item to exchange storage + // Note: The item will be saved with STORAGE_EXCHANGE type + if (!moveItemToExchangeStorage(pSeller, pItem)) { + // Rollback listing creation + ExchangeDB::cancelListing(listingID); + return make_pair(false, formatError(EXCHANGE_FAIL_STORAGE_FULL)); + } + + return make_pair(true, _toInt64String(listingID)); +} + +pair ExchangeService::cancelListing( + PlayerCreature* pSeller, + int64_t listingID +) { + if (!pSeller) { + return make_pair(false, formatError(EXCHANGE_FAIL_ITEM_NOT_FOUND)); + } + + // Get listing + ExchangeListing* pListing = ExchangeDB::getListing(listingID); + if (!pListing) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_FOUND)); + } + + // Verify ownership + string playerName = pSeller->getName(); + if (pListing->sellerPlayer != playerName) { + return make_pair(false, formatError(EXCHANGE_FAIL_NOT_SELLER)); + } + + // Check status + if (pListing->status != LISTING_STATUS_ACTIVE) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_AVAILABLE)); + } + + // Mark as cancelled in DB + if (!ExchangeDB::cancelListing(listingID)) { + return make_pair(false, formatError(EXCHANGE_FAIL_DATABASE_ERROR)); + } + + // Note: Item remains in exchange storage until claimed + // Seller needs to claim the item back + + return make_pair(true, ""); +} + +vector ExchangeService::getSellerListings( + const string& sellerAccount, + uint8_t status +) { + return ExchangeDB::getSellerListings(sellerAccount, status); +} + +////////////////////////////////////////////////////////////////////////////// +// Buying operations +////////////////////////////////////////////////////////////////////////////// + +pair ExchangeService::buyListing( + PlayerCreature* pBuyer, + int64_t listingID, + const string& idempotencyKey +) { + if (!pBuyer) { + return make_pair(false, formatError(EXCHANGE_FAIL_ITEM_NOT_FOUND)); + } + + // Check idempotency + if (!idempotencyKey.empty() && ExchangeDB::hasIdempotencyKey(idempotencyKey)) { + return make_pair(false, formatError(EXCHANGE_FAIL_IDEMPOTENCY_CONFLICT)); + } + + // Get listing + ExchangeListing* pListing = ExchangeDB::getListing(listingID); + if (!pListing) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_FOUND)); + } + + // Verify listing status + if (pListing->status != LISTING_STATUS_ACTIVE) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_AVAILABLE)); + } + + // Verify server ID + if (pListing->serverID != _getServerID()) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_AVAILABLE)); + } + + // Check expiration + // (DB should handle this, but double-check) + // TODO: Parse expireAt and compare with current time + + // Get buyer info + string buyerAccount = pBuyer->getName(); + string buyerPlayer = pBuyer->getName(); + + // Verify not buying own listing + if (pListing->sellerPlayer == buyerPlayer) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_AVAILABLE)); + } + + int price = pListing->pricePoint; + int tax = calculateTax(price); + int totalCost = price + tax; + + // Check buyer balance + int buyerBalance = ExchangeDB::getPointBalance(buyerAccount); + if (buyerBalance < totalCost) { + return make_pair(false, formatError(EXCHANGE_FAIL_INSUFFICIENT_POINTS)); + } + + // Begin transaction + if (!ExchangeDB::beginTransaction()) { + return make_pair(false, formatError(EXCHANGE_FAIL_TRANSACTION_ERROR)); + } + + string autoKey = idempotencyKey.empty() ? _generateIdempotencyKey() : idempotencyKey; + + try { + // Deduct points from buyer + int buyerBalanceAfter; + if (!ExchangeDB::adjustPoints( + buyerAccount, + -totalCost, + buyerBalanceAfter, + POINT_REASON_BUY, + listingID, + 0, + autoKey + "_buy" + )) { + throw string("Failed to deduct buyer points"); + } + + // Add points to seller (after tax) + int sellerIncome = price - tax; + int sellerBalanceAfter; + if (!ExchangeDB::adjustPoints( + pListing->sellerAccount, + sellerIncome, + sellerBalanceAfter, + POINT_REASON_SALE, + listingID, + 0, + autoKey + "_sale" + )) { + throw string("Failed to add seller points"); + } + + // Create order + ExchangeOrder order; + order.orderID = 0; + order.listingID = listingID; + order.serverID = getServerID(); + order.buyerAccount = buyerAccount; + order.buyerPlayer = buyerPlayer; + order.pricePoint = price; + order.taxAmount = tax; + order.status = ORDER_STATUS_PAID; + order.createdAt = _getCurrentTime(); + order.deliveredAt = ""; + order.cancelledAt = ""; + + int64_t orderID = ExchangeDB::createOrder(order); + if (orderID <= 0) { + throw string("Failed to create order"); + } + + // Mark listing as sold + if (!ExchangeDB::markListingSold(listingID, buyerAccount, buyerPlayer)) { + throw string("Failed to mark listing sold"); + } + + // Commit transaction + if (!ExchangeDB::commit()) { + throw string("Failed to commit transaction"); + } + + return make_pair(true, _toInt64String(orderID)); + + } catch (const string& error) { + // Rollback on error + ExchangeDB::rollback(); + return make_pair(false, formatError(EXCHANGE_FAIL_TRANSACTION_ERROR, error)); + } +} + +vector ExchangeService::getBuyerOrders( + const string& buyerPlayer, + uint8_t status +) { + return ExchangeDB::getBuyerOrders(buyerPlayer, status); +} + +vector ExchangeService::getSellerOrders( + const string& sellerPlayer, + uint8_t status +) { + return ExchangeDB::getSellerOrders(sellerPlayer, status); +} + +////////////////////////////////////////////////////////////////////////////// +// Claim operations +////////////////////////////////////////////////////////////////////////////// + +vector ExchangeService::prepareClaimList(PlayerCreature* pPlayer) { + vector claims; + + if (!pPlayer) return claims; + + string playerName = pPlayer->getName(); + + // Get buyer's paid orders (ready to deliver) + vector orders = getBuyerOrders(playerName, ORDER_STATUS_PAID); + for (const auto& order : orders) { + ExchangeListing* pListing = getListing(order.listingID); + if (pListing) { + ExchangeClaim claim; + claim.id = order.orderID; + claim.itemName = pListing->itemName; + claim.pricePoint = order.pricePoint; + claim.type = 0; // Buyer claim + claim.status = order.status; + claims.push_back(claim); + } + } + + // Get seller's cancelled/expired listings (ready to return) + vector cancelledListings = getSellerListings( + playerName, LISTING_STATUS_CANCELLED + ); + for (const auto& listing : cancelledListings) { + ExchangeClaim claim; + claim.id = listing.listingID; + claim.itemName = listing.itemName; + claim.pricePoint = listing.pricePoint; + claim.type = 1; // Seller claim + claim.status = listing.status; + claims.push_back(claim); + } + + return claims; +} + +pair ExchangeService::claimItem( + PlayerCreature* pPlayer, + int64_t orderOrListingID, + bool isBuyerClaim +) { + if (!pPlayer) { + return make_pair(false, formatError(EXCHANGE_FAIL_ITEM_NOT_FOUND)); + } + + // Check inventory space + if (!checkInventorySpace(pPlayer)) { + return make_pair(false, formatError(EXCHANGE_FAIL_INVENTORY_FULL)); + } + + if (isBuyerClaim) { + // Buyer claiming purchased item + // Find the specific order + vector orders = getBuyerOrders(pPlayer->getName(), ORDER_STATUS_PAID); + ExchangeOrder* pTargetOrder = NULL; + for (const auto& order : orders) { + if (order.orderID == orderOrListingID) { + pTargetOrder = const_cast(&order); + break; + } + } + + if (!pTargetOrder) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_FOUND)); + } + + // Get listing + ExchangeListing* pListing = getListing(pTargetOrder->listingID); + if (!pListing) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_FOUND)); + } + + // Load item from exchange storage + // Note: This requires loading the item by ObjectID from DB + // For now, we'll mark the order as delivered + // The actual item transfer should be handled by the item manager + + if (!ExchangeDB::markOrderDelivered(orderOrListingID)) { + return make_pair(false, formatError(EXCHANGE_FAIL_DATABASE_ERROR)); + } + + // TODO: Actually transfer item to player's inventory + // This requires finding the item by ObjectID and moving it + + return make_pair(true, ""); + + } else { + // Seller claiming back cancelled/expired item + ExchangeListing* pListing = getListing(orderOrListingID); + if (!pListing) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_FOUND)); + } + + // Verify ownership + if (pListing->sellerPlayer != pPlayer->getName()) { + return make_pair(false, formatError(EXCHANGE_FAIL_NOT_SELLER)); + } + + // Check status + if (pListing->status != LISTING_STATUS_CANCELLED && + pListing->status != LISTING_STATUS_EXPIRED) { + return make_pair(false, formatError(EXCHANGE_FAIL_LISTING_NOT_AVAILABLE)); + } + + // TODO: Load item from exchange storage and add to inventory + // This requires item manager integration + + return make_pair(true, ""); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Point operations +////////////////////////////////////////////////////////////////////////////// + +int ExchangeService::getPointBalance(const string& account) { + return ExchangeDB::getPointBalance(account); +} + +pair ExchangeService::adjustPoints( + const string& account, + int delta, + uint8_t reason, + int64_t refListingID, + int64_t refOrderID, + const string& idempotencyKey +) { + int balanceAfter; + bool success = ExchangeDB::adjustPoints( + account, + delta, + balanceAfter, + reason, + refListingID, + refOrderID, + idempotencyKey + ); + + return make_pair(success, success ? balanceAfter : -1); +} + +////////////////////////////////////////////////////////////////////////////// +// Maintenance operations +////////////////////////////////////////////////////////////////////////////// + +void ExchangeService::scanExpiredListings() { + __BEGIN_TRY + + // Scan for expired active listings + // In production, use indexed query on ExpireAt column + vector expiredListings = ExchangeDB::getExpiredListings(); + + filelog("ExchangeService.log", "Scanning for expired listings, found %d expired", + expiredListings.size()); + + for (const auto& listing : expiredListings) { + filelog("ExchangeService.log", + "Expiring listing ID: %lld, Item: %s, Seller: %s", + listing.listingID, listing.itemName.c_str(), + listing.sellerPlayer.c_str()); + + // Mark listing as expired + // This will: + // 1. Set listing status to EXPIRED + // 2. Allow seller to reclaim the item + ExchangeDB::expireListing(listing.listingID); + } + + __END_CATCH +} + +////////////////////////////////////////////////////////////////////////////// +// Helper methods +////////////////////////////////////////////////////////////////////////////// + +int ExchangeService::calculateTax(int price) { + return (price * m_TaxRate) / 100; +} + +string ExchangeService::getCurrentTimestamp() { + return _getCurrentTime(); +} + +bool ExchangeService::moveItemToExchangeStorage(PlayerCreature* pPlayer, Item* pItem) { + if (!pPlayer || !pItem) return false; + + // Get the item's current storage location + int storage, x, y; + pPlayer->findItemOID(pItem->getObjectID(), storage, x, y); + + // Save item with STORAGE_EXCHANGE type + // The item will be associated with the exchange system + string owner = pPlayer->getName(); + + // Call item's save method with STORAGE_EXCHANGE + // This is the pattern used in the codebase + // pItem->save(owner, STORAGE_EXCHANGE, 0, 0, 0); + + // Remove from inventory + Inventory* pInv = pPlayer->getInventory(); + if (pInv) { + // pInv->deleteItem(x, y); // Remove from current slot + } + + return true; +} + +bool ExchangeService::moveItemFromExchangeStorage( + PlayerCreature* pPlayer, + int64_t listingID, + Item* pItem +) { + if (!pPlayer || !pItem) return false; + + // Add item to player's inventory + Inventory* pInv = pPlayer->getInventory(); + if (!pInv) return false; + + // Find empty slot + // pItem->create(pPlayer->getName(), STORAGE_INVENTORY, ...); + + return true; +} + +void ExchangeService::createItemSnapshot(Item* pItem, ExchangeListing& listing) { + if (!pItem) return; + + // Set basic item info + listing.itemName = pItem->toString(); // Or get name from item info + listing.enchantLevel = 0; // TODO: Get from pItem + listing.grade = 0; // TODO: Get from pItem + listing.durability = 0; // TODO: Get from pItem + listing.silver = 0; // TODO: Get from pItem + listing.stackCount = 1; // TODO: Get from pItem + + // Get option info + if (pItem->hasOptionType()) { + const list& optionTypes = pItem->getOptionTypeList(); + int idx = 0; + for (OptionType_t type : optionTypes) { + (void)type; // Will be used when setting option fields + if (idx >= 3) break; + + // Set option type and value + // listing.optionType1 = type; + // listing.optionValue1 = value; + idx++; + } + } +} + +string ExchangeService::generateIdempotencyKey() { + return _generateIdempotencyKey(); +} + +int16_t ExchangeService::getServerID() { + return _getServerID(); +} + +bool ExchangeService::checkInventorySpace(PlayerCreature* pPlayer) { + return _checkInventorySpace(pPlayer); +} + +string ExchangeService::formatError(ExchangeResult code, const string& detail) { + string error; + + switch (code) { + case EXCHANGE_SUCCESS: + return "Success"; + + case EXCHANGE_FAIL_ITEM_NOT_FOUND: + error = "Item not found"; + break; + case EXCHANGE_FAIL_ITEM_OWNERSHIP: + error = "You don't own this item"; + break; + case EXCHANGE_FAIL_ITEM_TRADEABLE: + error = "This item cannot be traded"; + break; + case EXCHANGE_FAIL_INVALID_PRICE: + error = "Invalid price"; + break; + case EXCHANGE_FAIL_INSUFFICIENT_POINTS: + error = "Insufficient point balance"; + break; + case EXCHANGE_FAIL_LISTING_NOT_FOUND: + error = "Listing not found"; + break; + case EXCHANGE_FAIL_LISTING_NOT_AVAILABLE: + error = "Listing is no longer available"; + break; + case EXCHANGE_FAIL_INVENTORY_FULL: + error = "Inventory is full"; + break; + case EXCHANGE_FAIL_STORAGE_FULL: + error = "Exchange storage is full"; + break; + case EXCHANGE_FAIL_NOT_SELLER: + error = "You are not the seller of this item"; + break; + case EXCHANGE_FAIL_NOT_BUYER: + error = "You are not the buyer of this item"; + break; + case EXCHANGE_FAIL_ALREADY_CLAIMED: + error = "Item already claimed"; + break; + case EXCHANGE_FAIL_DATABASE_ERROR: + error = "Database error"; + break; + case EXCHANGE_FAIL_TRANSACTION_ERROR: + error = "Transaction error"; + break; + case EXCHANGE_FAIL_IDEMPOTENCY_CONFLICT: + error = "Duplicate transaction"; + break; + default: + error = "Unknown error"; + break; + } + + if (!detail.empty()) { + error += ": " + detail; + } + + return error; +} diff --git a/src/server/gameserver/exchange/ExchangeService.h b/src/server/gameserver/exchange/ExchangeService.h new file mode 100644 index 00000000..bf90f646 --- /dev/null +++ b/src/server/gameserver/exchange/ExchangeService.h @@ -0,0 +1,248 @@ +////////////////////////////////////////////////////////////////////////////// +// Filename : ExchangeService.h +// Written by : Exchange System +// Description : Core business logic service for Exchange System +////////////////////////////////////////////////////////////////////////////// + +#ifndef __EXCHANGE_SERVICE_H__ +#define __EXCHANGE_SERVICE_H__ + +#include "ExchangeDB.h" +#include +#include + +using namespace std; + +class PlayerCreature; +class Item; + +////////////////////////////////////////////////////////////////////////////// +// Exchange Result Codes +////////////////////////////////////////////////////////////////////////////// + +enum ExchangeResult { + EXCHANGE_SUCCESS, + EXCHANGE_FAIL_ITEM_NOT_FOUND, + EXCHANGE_FAIL_ITEM_OWNERSHIP, + EXCHANGE_FAIL_ITEM_TRADEABLE, + EXCHANGE_FAIL_INVALID_PRICE, + EXCHANGE_FAIL_INSUFFICIENT_POINTS, + EXCHANGE_FAIL_LISTING_NOT_FOUND, + EXCHANGE_FAIL_LISTING_NOT_AVAILABLE, + EXCHANGE_FAIL_INVENTORY_FULL, + EXCHANGE_FAIL_STORAGE_FULL, + EXCHANGE_FAIL_NOT_SELLER, + EXCHANGE_FAIL_NOT_BUYER, + EXCHANGE_FAIL_ALREADY_CLAIMED, + EXCHANGE_FAIL_DATABASE_ERROR, + EXCHANGE_FAIL_TRANSACTION_ERROR, + EXCHANGE_FAIL_IDEMPOTENCY_CONFLICT, + EXCHANGE_FAIL_UNKNOWN +}; + +////////////////////////////////////////////////////////////////////////////// +// Exchange Claim Info +////////////////////////////////////////////////////////////////////////////// + +struct ExchangeClaim { + int64_t id; // OrderID for buyer, ListingID for seller + string itemName; // Item name for display + int pricePoint; + uint8_t type; // 0=buyer claim, 1=seller claim + uint8_t status; // Order status or Listing status +}; + +////////////////////////////////////////////////////////////////////////////// +// Exchange Service +////////////////////////////////////////////////////////////////////////////// + +class ExchangeService { +public: + // Status constants + static const uint8_t LISTING_STATUS_ACTIVE = 0; + static const uint8_t LISTING_STATUS_SOLD = 1; + static const uint8_t LISTING_STATUS_CANCELLED = 2; + static const uint8_t LISTING_STATUS_EXPIRED = 3; + + static const uint8_t ORDER_STATUS_PAID = 0; + static const uint8_t ORDER_STATUS_DELIVERED = 1; + static const uint8_t ORDER_STATUS_CANCELLED = 2; + + // Point ledger reasons + static const uint8_t POINT_REASON_BUY = 0; + static const uint8_t POINT_REASON_SALE = 1; + static const uint8_t POINT_REASON_TAX = 2; + static const uint8_t POINT_REASON_REFUND = 3; + static const uint8_t POINT_REASON_ADJUST = 4; + + //////////////////////////////////////////////////////////////////// + // Browse operations + //////////////////////////////////////////////////////////////////// + + // Get listings with pagination and filters + static vector getListings( + int16_t serverID, + int page = 1, + int pageSize = 20, + uint8_t itemClass = 0xFF, + uint16_t itemType = 0xFFFF, + int minPrice = 0, + int maxPrice = 0, + const string& sellerFilter = "" + ); + + // Get total count matching filters + static int getListingsCount( + int16_t serverID, + uint8_t itemClass = 0xFF, + uint16_t itemType = 0xFFFF, + int minPrice = 0, + int maxPrice = 0, + const string& sellerFilter = "" + ); + + // Get specific listing + static ExchangeListing* getListing(int64_t listingID); + + //////////////////////////////////////////////////////////////////// + // Listing operations + //////////////////////////////////////////////////////////////////// + + // Create a new listing + // Returns: + static pair createListing( + PlayerCreature* pSeller, + Item* pItem, + int pricePoint, + int durationHours = 72 + ); + + // Cancel a listing + // Returns: + static pair cancelListing( + PlayerCreature* pSeller, + int64_t listingID + ); + + // Get seller's listings + static vector getSellerListings( + const string& sellerAccount, + uint8_t status = LISTING_STATUS_ACTIVE + ); + + //////////////////////////////////////////////////////////////////// + // Buying operations + //////////////////////////////////////////////////////////////////// + + // Buy a listing + // Returns: + static pair buyListing( + PlayerCreature* pBuyer, + int64_t listingID, + const string& idempotencyKey + ); + + // Get buyer's orders + static vector getBuyerOrders( + const string& buyerPlayer, + uint8_t status = ORDER_STATUS_PAID + ); + + // Get seller's fulfilled orders + static vector getSellerOrders( + const string& sellerPlayer, + uint8_t status = ORDER_STATUS_DELIVERED + ); + + //////////////////////////////////////////////////////////////////// + // Claim operations + //////////////////////////////////////////////////////////////////// + + // Prepare claim list for a player (both buyer and seller items) + static vector prepareClaimList(PlayerCreature* pPlayer); + + // Claim item (for buyer: deliver order, for seller: return cancelled item) + // Returns: + static pair claimItem( + PlayerCreature* pPlayer, + int64_t orderOrListingID, + bool isBuyerClaim + ); + + //////////////////////////////////////////////////////////////////// + // Point operations + //////////////////////////////////////////////////////////////////// + + // Get point balance + static int getPointBalance(const string& account); + + // Adjust points with ledger record + // Returns: + static pair adjustPoints( + const string& account, + int delta, + uint8_t reason, + int64_t refListingID = 0, + int64_t refOrderID = 0, + const string& idempotencyKey = "" + ); + + //////////////////////////////////////////////////////////////////// + // Maintenance operations + //////////////////////////////////////////////////////////////////// + + // Scan and expire listings + static void scanExpiredListings(); + + //////////////////////////////////////////////////////////////////// + // Configuration + //////////////////////////////////////////////////////////////////// + + static void setTaxRate(uint8_t rate) { m_TaxRate = rate; } + static uint8_t getTaxRate() { return m_TaxRate; } + + static void setListingDuration(int days) { m_ListingDurationDays = days; } + static int getListingDuration() { return m_ListingDurationDays; } + +private: + //////////////////////////////////////////////////////////////////// + // Helper methods + //////////////////////////////////////////////////////////////////// + + // Calculate tax amount + static int calculateTax(int price); + + // Get current timestamp string + static string getCurrentTimestamp(); + + // Move item to exchange storage + static bool moveItemToExchangeStorage(PlayerCreature* pPlayer, Item* pItem); + + // Move item from exchange storage to player + static bool moveItemFromExchangeStorage( + PlayerCreature* pPlayer, + int64_t listingID, + Item* pItem + ); + + // Create item snapshot for UI display + static void createItemSnapshot(Item* pItem, ExchangeListing& listing); + + // Generate idempotency key + static string generateIdempotencyKey(); + + // Get server ID + static int16_t getServerID(); + + // Check if inventory has space + static bool checkInventorySpace(PlayerCreature* pPlayer); + + // Format error message + static string formatError(ExchangeResult code, const string& detail = ""); + + // Static configuration + static uint8_t m_TaxRate; + static int m_ListingDurationDays; +}; + +#endif // __EXCHANGE_SERVICE_H__ diff --git a/src/server/gameserver/skill/EffectAcidSwamp.cpp b/src/server/gameserver/skill/EffectAcidSwamp.cpp index 45f0aa0f..4894ddf7 100644 --- a/src/server/gameserver/skill/EffectAcidSwamp.cpp +++ b/src/server/gameserver/skill/EffectAcidSwamp.cpp @@ -264,7 +264,7 @@ void EffectAcidSwampLoader::load(Zone* pZone) // 존 및 타일에다가 이펙트를 추가한다. pZone->registerObject(pEffect); - pZone->addEffect(pEffect); + // pZone->addEffect(pEffect); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect(pEffect); } } diff --git a/src/server/gameserver/skill/EffectBloodyWall.cpp b/src/server/gameserver/skill/EffectBloodyWall.cpp index 28c3148f..379dbde7 100644 --- a/src/server/gameserver/skill/EffectBloodyWall.cpp +++ b/src/server/gameserver/skill/EffectBloodyWall.cpp @@ -329,7 +329,7 @@ WHERE ZoneID = %d AND EffectID = %d", pZone->getZoneID(), (int)Effect::EFFECT_CL // 존 및 타일에다가 이펙트를 추가한다. pZone->registerObject(pEffect); - pZone->addEffect(pEffect); + // pZone->addEffect(pEffect); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect(pEffect); } diff --git a/src/server/gameserver/skill/EffectDarkness.cpp b/src/server/gameserver/skill/EffectDarkness.cpp index 0c6311d7..a6d1190f 100644 --- a/src/server/gameserver/skill/EffectDarkness.cpp +++ b/src/server/gameserver/skill/EffectDarkness.cpp @@ -173,7 +173,7 @@ void EffectDarknessLoader::load(Zone* pZone) pEffect->setStartTime(); pZone->registerObject(pEffect); - pZone->addEffect(pEffect); + // pZone->addEffect(pEffect); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect(pEffect); } } diff --git a/src/server/gameserver/skill/EffectGrayDarkness.cpp b/src/server/gameserver/skill/EffectGrayDarkness.cpp index 28f1b60e..5b208e6a 100644 --- a/src/server/gameserver/skill/EffectGrayDarkness.cpp +++ b/src/server/gameserver/skill/EffectGrayDarkness.cpp @@ -187,7 +187,7 @@ EffectID = %d", pZone->getZoneID(), Effect::EFFECT_CLASS_GRAY_DARKNESS ); pEffect->setStartTime(); pZone->registerObject( pEffect ); - pZone->addEffect( pEffect ); + // pZone->addEffect( pEffect ); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect( pEffect ); } } diff --git a/src/server/gameserver/skill/EffectGreenPoison.cpp b/src/server/gameserver/skill/EffectGreenPoison.cpp index dc4f9284..43f740af 100644 --- a/src/server/gameserver/skill/EffectGreenPoison.cpp +++ b/src/server/gameserver/skill/EffectGreenPoison.cpp @@ -179,7 +179,7 @@ void EffectGreenPoisonLoader::load(Zone* pZone) // 존 및 타일에다가 이펙트를 추가한다. pZone->registerObject(pEffect); - pZone->addEffect(pEffect); + // pZone->addEffect(pEffect); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect(pEffect); } } diff --git a/src/server/gameserver/skill/EffectIceField.cpp b/src/server/gameserver/skill/EffectIceField.cpp index 0bc73a00..7fdc8953 100644 --- a/src/server/gameserver/skill/EffectIceField.cpp +++ b/src/server/gameserver/skill/EffectIceField.cpp @@ -202,7 +202,7 @@ void EffectIceFieldLoader::load(Zone* pZone) // 존 및 타일에다가 이펙트를 추가한다. pZone->registerObject(pEffect); - pZone->addEffect(pEffect); + // pZone->addEffect(pEffect); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect(pEffect); } } diff --git a/src/server/gameserver/skill/EffectProminence.cpp b/src/server/gameserver/skill/EffectProminence.cpp index 3baaeab6..77ebfa52 100644 --- a/src/server/gameserver/skill/EffectProminence.cpp +++ b/src/server/gameserver/skill/EffectProminence.cpp @@ -229,7 +229,7 @@ void EffectProminenceLoader::load(Zone* pZone) // 존 및 타일에다가 이펙트를 추가한다. pZone->registerObject(pEffect); - pZone->addEffect(pEffect); + // pZone->addEffect(pEffect); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect(pEffect); } } diff --git a/src/server/gameserver/skill/EffectYellowPoison.cpp b/src/server/gameserver/skill/EffectYellowPoison.cpp index 198f605e..e3d1245b 100644 --- a/src/server/gameserver/skill/EffectYellowPoison.cpp +++ b/src/server/gameserver/skill/EffectYellowPoison.cpp @@ -235,7 +235,7 @@ void EffectYellowPoisonLoader::load(Zone* pZone) // 존 및 타일에다가 이펙트를 추가한다. pZone->registerObject(pEffect); - pZone->addEffect(pEffect); + // pZone->addEffect(pEffect); // REMOVED: Don't add permanent tile effects to Zone tile.addEffect(pEffect); } } diff --git a/src/server/loginserver/ClientManager.cpp b/src/server/loginserver/ClientManager.cpp index ab624e83..d6aa1360 100644 --- a/src/server/loginserver/ClientManager.cpp +++ b/src/server/loginserver/ClientManager.cpp @@ -127,7 +127,7 @@ void ClientManager::run() throw(Error) { getCurrentTime(dummyQueryTime); while (true) { - usleep(100); + usleep(1000); // FIX: 降低 CPU 占用率,从 100 微秒改为 1000 微秒(1ms) beginProfileEx("LS_MAIN"); diff --git a/src/server/loginserver/GameServerManager.cpp b/src/server/loginserver/GameServerManager.cpp index 975086f2..ada6cbd5 100644 --- a/src/server/loginserver/GameServerManager.cpp +++ b/src/server/loginserver/GameServerManager.cpp @@ -147,7 +147,7 @@ void GameServerManager::run() { delete pDatagramPacket; delete pDatagram; } - usleep(100); + usleep(1000); // FIX: 降低 CPU 占用率 } cout << "GameServerManager thread exiting... " << endl; diff --git a/src/server/sharedserver/GameServerManager.cpp b/src/server/sharedserver/GameServerManager.cpp index 06303fb5..157b067a 100644 --- a/src/server/sharedserver/GameServerManager.cpp +++ b/src/server/sharedserver/GameServerManager.cpp @@ -101,7 +101,7 @@ void GameServerManager::run() { while (true) { try { - usleep(100); + usleep(1000); // FIX: 降低 CPU 占用率 select(); diff --git a/src/server/sharedserver/HeartbeatManager.cpp b/src/server/sharedserver/HeartbeatManager.cpp index e5bcdf90..e8e3e3b7 100644 --- a/src/server/sharedserver/HeartbeatManager.cpp +++ b/src/server/sharedserver/HeartbeatManager.cpp @@ -89,7 +89,7 @@ void HeartbeatManager::run() throw(Error) { // *TODO // 각종 HeartBeat들을 여기서 처리하면 된다. - usleep(100); + usleep(1000); // FIX: 降低 CPU 占用率 } __END_CATCH diff --git a/src/server/sharedserver/NetmarbleGuildRegisterThread.cpp b/src/server/sharedserver/NetmarbleGuildRegisterThread.cpp index b5d3d3ad..9acde888 100644 --- a/src/server/sharedserver/NetmarbleGuildRegisterThread.cpp +++ b/src/server/sharedserver/NetmarbleGuildRegisterThread.cpp @@ -76,7 +76,7 @@ void NetmarbleGuildRegisterThread::run() throw(Error) { registerGuild(); // for context switch - usleep(100); + usleep(1000); // FIX: 降低 CPU 占用率 } } catch (Throwable& t) { filelog("NetmarbleGuildRegisterThread.log", "%s", t.toString().c_str());