Browse Source

2014 Client Support
* Implemented the Roulette Game feature.
* Implemented per-character sex change feature.
* More info: https://rathena.org/board/topic/102247-2014-client-support/
- Special thanks to HerculesWS/Hercules@239d480 for the base.

aleos89 10 năm trước cách đây
mục cha
commit
3d9c6e7763

+ 5 - 0
conf/battle/feature.conf

@@ -57,3 +57,8 @@ feature.autotrade_sit: 1
 
 // Delay in miliseconds to open vending/buyingsotre after player logged in.
 feature.autotrade_open_delay: 5000
+
+// Roulette (Note 1)
+// Requires: 2014-10-22bRagexe or later
+// Off by default while test version is out; enable at your own risk.
+feature.roulette: off

+ 1 - 0
conf/inter_athena.conf

@@ -139,6 +139,7 @@ mapreg_db: mapreg
 vending_db: vendings
 vending_items_db: vending_items
 market_table: market
+db_roulette_table: db_roulette
 
 // Use SQL item_db, mob_db and mob_skill_db for the map server? (yes/no)
 use_sql_db: no

+ 23 - 22
conf/log_athena.conf

@@ -8,31 +8,32 @@
 //--------------------------------------------------------------
 
 // Enable Logs? (Note 3)
-// 0x00000 - Don't log at all
-// 0x00001 - (T) Log trades
-// 0x00002 - (V) Log vending transactions
-// 0x00004 - (P) Log items drop/picked by players
-// 0x00008 - (L) Log items drop/looted by monsters
-// 0x00010 - (S) Log NPC transactions (buy/sell)
-// 0x00020 - (N) Log Script transactions (items deleted/acquired through quests)
-// 0x00040 - (D) Log items stolen from mobs (Steal/Gank)
-// 0x00080 - (C) Log player-used items (consumables/pet&hom&merc food/items used for skills&attacks)
-// 0x00100 - (O) Log produced/ingredient items
-// 0x00200 - (U) Log MVP prize items
-// 0x00400 - (A) Log player created/deleted items (through @/# commands)
-// 0x00800 - (R) Log items placed/retrieved from storage.
-// 0x01000 - (G) Log items placed/retrieved from guild storage.
-// 0x02000 - (E) Log mail system transactions.
-// 0x04000 - (I) Log auction system transactions.
-// 0x08000 - (B) Log buying store transactions
-// 0x10000 - (X) Log all other transactions (rentals expiring/inserting cards/items removed by item_check/
+// 0x000000 - Don't log at all
+// 0x000001 - (T) Log trades
+// 0x000002 - (V) Log vending transactions
+// 0x000004 - (P) Log items drop/picked by players
+// 0x000008 - (L) Log items drop/looted by monsters
+// 0x000010 - (S) Log NPC transactions (buy/sell)
+// 0x000020 - (N) Log Script transactions (items deleted/acquired through quests)
+// 0x000040 - (D) Log items stolen from mobs (Steal/Gank)
+// 0x000080 - (C) Log player-used items (consumables/pet&hom&merc food/items used for skills&attacks)
+// 0x000100 - (O) Log produced/ingredient items
+// 0x000200 - (U) Log MVP prize items
+// 0x000400 - (A) Log player created/deleted items (through @/# commands)
+// 0x000800 - (R) Log items placed/retrieved from storage.
+// 0x001000 - (G) Log items placed/retrieved from guild storage.
+// 0x002000 - (E) Log mail system transactions.
+// 0x004000 - (I) Log auction system transactions.
+// 0x008000 - (B) Log buying store transactions
+// 0x010000 - (X) Log all other transactions (rentals expiring/inserting cards/items removed by item_check/
 //           rings deleted by divorce/pet egg (un)hatching/pet armor (un)equipping/Weapon Refine skill/Remove Trap skill)
-// 0x20000 - ($) Log cash transactions
-// 0x40000 - (K) Log account bank transactions
-// 0x80000 - (F) Removed bound items when guild/party is broken
+// 0x020000 - ($) Log cash transactions
+// 0x040000 - (K) Log account bank transactions
+// 0x080000 - (F) Removed bound items when guild/party is broken
+// 0x100000 - (Y) Roulette Lottery
 // Example: Log trades+vending+script items+created items: 1+2+32+1024 = 1059
 // Please note that moving items from inventory to cart and back is not logged by design.
-enable_logs: 0xFFFFF
+enable_logs: 0xFFFFFF
 
 // Use MySQL Logs? [SQL Version Only] (Note 1)
 sql_logs: yes

+ 3 - 0
conf/msg_conf/map_msg.conf

@@ -1593,5 +1593,8 @@
 1495: You can't withdraw that much money
 1496: Banking is disabled
 
+// Roulette
+1497: Roulette is disabled
+
 //Custom translations
 //import: conf/msg_conf/import/map_msg_eng_conf.txt

+ 3 - 0
db/const.txt

@@ -432,6 +432,9 @@ CharMoves	124	1
 CharRename	125	1
 Font		126	1
 BankVault	127	1
+RouletteBronze	128	1
+RouletteSilver	129	1
+RouletteGold	130	1
 
 bMaxHP	6
 bMaxSP	8

+ 78 - 1
db/packet_db.txt

@@ -2318,5 +2318,82 @@ packet_keys: 0x631C511C,0x111C111C,0x111C111C // [Shakto]
 0x09D8,2,npcmarketclosed,0
 0x09DF,7
 
+//2014-10-16Ragexe
+packet_ver: 50
+0x0369,7,actionrequest,2,6
+0x083C,10,useskilltoid,2,4,6
+0x0437,5,walktoxy,2
+0x035F,6,ticksend,2
+0x0967,5,changedir,2,4
+0x07E4,6,takeitem,2
+0x0362,6,dropitem,2,4
+0x07EC,8,movetokafra,2,4
+0x022D,8,movefromkafra,2,4
+0x0438,10,useskilltopos,2,4,6,8
+0x0366,90,useskilltoposinfo,2,4,6,8,10
+0x096A,6,getcharnamerequest,2
+0x0368,6,solvecharname,2
+0x0838,12,searchstoreinfolistitemclick,2,6,10
+0x0835,2,searchstoreinfonextpage,0
+0x0819,-1,searchstoreinfo,2,4,5,9,13,14,15
+0x0811,-1,reqtradebuyingstore,2,4,8,12
+0x0360,6,reqclickbuyingstore,2
+0x0817,2,reqclosebuyingstore,0
+0x0815,-1,reqopenbuyingstore,2,4,8,9,89
+0x0365,18,bookingregreq,2,4
+// 0x0363,8 // CZ_JOIN_BATTLE_FIELD
+0x0281,-1,itemlistwindowselected,2,4,8
+0x086E,19,wanttoconnection,2,6,10,14,18
+0x0802,26,partyinvite,2
+// 0x0922,4 // CZ_GANGSI_RANK
+0x094B,26,friendslistadd,2
+0x0364,5,hommenu,2,4
+0x0936,36,storagepassword,0
+0x09DF,7
+0x0a00,269
+// Roulette System [Yommy]
+0x0A19,2,rouletteopen,0 // HEADER_CZ_REQ_OPEN_ROULETTE
+0x0A1A,23 // HEADER_ZC_ACK_OPEN_ROULETTE
+0x0A1B,2,rouletteinfo,0 // HEADER_CZ_REQ_ROULETTE_INFO
+0x0A1C,-1 // HEADER_ZC_ACK_ROULETTE_INFO
+0x0A1D,2,rouletteclose,0 // HEADER_CZ_REQ_CLOSE_ROULETTE
+0x0A1E,3 // HEADER_ZC_ACK_CLOSE_ROULETTE
+0x0A1F,2,roulettegenerate,0 // HEADER_CZ_REQ_GENERATE_ROULETTE
+0x0A20,21 // HEADER_ZC_ACK_GENERATE_ROULETTE
+0x0A21,3,rouletterecvitem,2 // HEADER_CZ_RECV_ROULETTE_ITEM
+0x0A22,5 // HEADER_ZC_RECV_ROULETTE_ITEM
+
+//2014-10-22bRagexe
+packet_ver: 51
+0x0369,7,actionrequest,2,6
+0x083C,10,useskilltoid,2,4,6
+0x0437,5,walktoxy,2
+0x035F,6,ticksend,2
+0x08AD,5,changedir,2,4
+0x094E,6,takeitem,2
+0x087D,6,dropitem,2,4
+0x0878,8,movetokafra,2,4
+0x08AA,8,movefromkafra,2,4
+0x023B,10,useskilltopos,2,4,6,8
+0x0366,90,useskilltoposinfo,2,4,6,8,10
+0x096A,6,getcharnamerequest,2
+0x0368,6,solvecharname,2
+0x0835,12,searchstoreinfolistitemclick,2,6,10
+0x0940,2,searchstoreinfonextpage,0
+0x0819,-1,searchstoreinfo,2,4,5,9,13,14,15
+0x0811,-1,reqtradebuyingstore,2,4,8,12
+0x0360,6,reqclickbuyingstore,2
+0x0817,2,reqclosebuyingstore,0
+0x0815,-1,reqopenbuyingstore,2,4,8,9,89
+0x0955,18,bookingregreq,2,4
+// 0x092B,8 // CZ_JOIN_BATTLE_FIELD
+0x0281,-1,itemlistwindowselected,2,4,8
+0x093B,19,wanttoconnection,2,6,10,14,18
+0x0896,26,partyinvite,2
+// 0x08AB,4 // CZ_GANGSI_RANK
+0x091A,26,friendslistadd,2
+0x0899,5,hommenu,2,4
+0x0438,36,storagepassword,0
+
 //Add new packets here
-//packet_ver: 47
+//packet_ver: 52

+ 3 - 3
db/re/item_db.txt

@@ -165,11 +165,11 @@
 668,Handsei,Red Envelope,2,0,,20,,,,,0xFFFFFFFF,63,2,,,,,,{ set Zeny,Zeny+rand(1000,10000); },{},{}
 669,Rice_Cake_Soup,Tempting Rice-Cake Soup,0,500,,100,,,,,0xFFFFFFFF,63,2,,,,,,{ percentheal -100,-100; },{},{}
 670,Gold_Coin_Moneybag,Bag of Gold Coins,3,100000,,400,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
-671,Gold_Coin,Gold Coin,3,10000,,0,,,,,0xFFFFFFFF,63,2,,,,,,{ /*goldpoint++; (For Roulette game)*/ },{},{}
+671,Gold_Coin,Gold Coin,3,10000,,0,,,,,0xFFFFFFFF,63,2,,,,,,{ RouletteGold++; },{},{}
 672,Copper_Coin_Moneybag,Bag of Bronze Coins,3,1000,,400,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
-673,Copper_Coin,Bronze Coin,3,100,,40,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
+673,Copper_Coin,Bronze Coin,3,100,,40,,,,,0xFFFFFFFF,63,2,,,,,,{ RouletteBronze++; },{},{}
 674,Mithril_Coin,Mithril Coin,3,5000,,40,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
-675,Silver_Coin,Silver Coin,3,5000,,0,,,,,0xFFFFFFFF,63,2,,,,,,{ /*silverpoint++; (For Roulette game)*/ },{},{}
+675,Silver_Coin,Silver Coin,3,5000,,0,,,,,0xFFFFFFFF,63,2,,,,,,{ RouletteSilver++; },{},{}
 676,Silver_Coin_Moneybag,Bag of Silver Coins,3,50000,,400,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
 677,White_Gold_Coin,Platinum Coin,3,2000,,40,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
 678,Poison_Bottle,Poison Bottle,2,5000,,100,,,,,0xFFFFFFFF,63,2,,,,,,{ if(Class == Job_Assassin_Cross || Class == Job_Guillotine_Cross || Class == Job_Guillotine_Cross_T) { sc_start SC_DPOISON,60000,0; sc_start SC_ASPDPOTION3,60000,9; } else percentheal -100,-100; },{},{}

+ 14 - 0
doc/script_commands.txt

@@ -4169,6 +4169,20 @@ they will also have their skills reset upon 'changesex'.
 
 ---------------------------------------
 
+*changecharsex({<char_id>});
+
+This command will change the gender of the attached character. If it 
+was male, it will become female, if it was female, it will become male. The 
+change will be written to the character server, the player will receive the 
+message: "Need disconnection to perform change-sex request..." and the player 
+will be immediately kicked to the login screen. When they log back in, they will 
+be the opposite sex.
+
+If the character being changed is a Dancer/Gypsy or Bard/Clown class type, 
+the character will also have their skills reset upon 'changecharsex'.
+
+---------------------------------------
+
 *getexp <base xp>,<job xp>{,<char_id>};
 
 This command will give the invoking character a specified number of base and job 

+ 66 - 0
sql-files/main.sql

@@ -125,6 +125,7 @@ CREATE TABLE IF NOT EXISTS `char` (
   `unban_time` int(11) unsigned NOT NULL default '0',
   `font` tinyint(3) unsigned NOT NULL default '0',
   `uniqueitem_counter` int(11) unsigned NOT NULL default '0',
+  `sex` ENUM('M','F','U') NOT NULL default 'U',
   PRIMARY KEY  (`char_id`),
   UNIQUE KEY `name_key` (`name`),
   KEY `account_id` (`account_id`),
@@ -637,6 +638,71 @@ CREATE TABLE IF NOT EXISTS `ragsrvinfo` (
   `drop` int(11) unsigned NOT NULL default '0'
 ) ENGINE=MyISAM;
 
+--
+-- Table structure for `db_roulette`
+--
+CREATE TABLE `db_roulette` (
+  `level` smallint(5) unsigned NOT NULL,
+  `item_id` smallint(5) unsigned NOT NULL,
+  `amount` smallint(5) unsigned NOT NULL DEFAULT '1',
+  `flag` smallint(5) unsigned NOT NULL DEFAULT '1',
+  PRIMARY KEY (`level`,`item_id`)
+) ENGINE=MyISAM;
+
+-- ----------------------------
+-- Records of db_roulette
+-- ----------------------------
+-- Info: http://ro.gnjoy.com/news/update/View.asp?seq=157&curpage=1
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 675, 1, 1 ); -- Silver_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 671, 1, 0 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 678, 1, 0 ); -- Poison_Bottle
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 604, 1, 0 ); -- Branch_Of_Dead_Tree
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 522, 1, 0 ); -- Fruit_Of_Mastela
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 671, 1, 0 ); -- Old_Ore_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 12523, 1, 0 ); -- E_Inc_Agi_10_Scroll
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 985, 1, 0 ); -- Elunium
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 984, 1, 0 ); -- Oridecon
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 675, 1, 1 ); -- Silver_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 671, 1, 0 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 603, 1, 0 ); -- Old_Blue_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 608, 1, 0 ); -- Seed_Of_Yggdrasil
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 607, 1, 0 ); -- Yggdrasilberry
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 12522, 1, 0 ); -- E_Blessing_10_Scroll
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 6223, 1, 0 ); -- Carnium
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 6224, 1, 0 ); -- Bradium
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 675, 1, 1 ); -- Silver_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 671, 1, 0 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 12108, 1, 0 ); -- Bundle_Of_Magic_Scroll
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 617, 1, 0 ); -- Old_Violet_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 12514, 1, 0 ); -- E_Abrasive
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 7444, 1, 0 ); -- Treasure_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 969, 1, 0 ); -- Gold
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 675, 1, 1 ); -- Silver_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 671, 1, 0 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 616, 1, 0 ); -- Old_Card_Album
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 12516, 1, 0 ); -- E_Small_Life_Potion
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 22777, 1, 0 ); -- Gift_Buff_Set
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 6231, 1, 0 ); -- Guarantee_Weapon_6Up
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 671, 1, 1 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 12246, 1, 0 ); -- Magic_Card_Album
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 12263, 1, 0 ); -- Comp_Battle_Manual
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 671, 1, 0 ); -- Potion_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 6235, 1, 0 ); -- Guarantee_Armor_6Up
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 6, 671, 1, 1 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 6, 12766, 1, 0 ); -- Reward_Job_BM25
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 6, 6234, 1, 0 ); -- Guarantee_Armor_7Up
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 6, 6233, 1, 0 ); -- Guarantee_Armor_8Up
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 7, 671, 1, 1 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 7, 6233, 1, 0 ); -- Guarantee_Armor_8Up
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 7, 6233, 1, 0 ); -- Guarantee_Armor_8Up	// KRO lists this twice
+
 --
 -- Table structure for table `skill`
 --

+ 63 - 0
sql-files/upgrades/upgrade_20150619.sql

@@ -0,0 +1,63 @@
+ALTER TABLE  `char` ADD COLUMN `sex` ENUM('M','F','U') NOT NULL default 'U';
+
+CREATE TABLE `db_roulette` (
+  `level` smallint(5) unsigned NOT NULL,
+  `item_id` smallint(5) unsigned NOT NULL,
+  `amount` smallint(5) unsigned NOT NULL DEFAULT '1',
+  `flag` smallint(5) unsigned NOT NULL DEFAULT '1',
+  PRIMARY KEY (`level`,`item_id`)
+) ENGINE=MyISAM;
+
+-- ----------------------------
+-- Records of db_roulette
+-- ----------------------------
+-- Info: http://ro.gnjoy.com/news/update/View.asp?seq=157&curpage=1
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 675, 1, 1 ); -- Silver_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 671, 1, 0 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 678, 1, 0 ); -- Poison_Bottle
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 604, 1, 0 ); -- Branch_Of_Dead_Tree
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 522, 1, 0 ); -- Fruit_Of_Mastela
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 671, 1, 0 ); -- Old_Ore_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 12523, 1, 0 ); -- E_Inc_Agi_10_Scroll
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 985, 1, 0 ); -- Elunium
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 1, 984, 1, 0 ); -- Oridecon
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 675, 1, 1 ); -- Silver_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 671, 1, 0 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 603, 1, 0 ); -- Old_Blue_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 608, 1, 0 ); -- Seed_Of_Yggdrasil
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 607, 1, 0 ); -- Yggdrasilberry
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 12522, 1, 0 ); -- E_Blessing_10_Scroll
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 6223, 1, 0 ); -- Carnium
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 2, 6224, 1, 0 ); -- Bradium
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 675, 1, 1 ); -- Silver_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 671, 1, 0 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 12108, 1, 0 ); -- Bundle_Of_Magic_Scroll
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 617, 1, 0 ); -- Old_Violet_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 12514, 1, 0 ); -- E_Abrasive
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 7444, 1, 0 ); -- Treasure_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 3, 969, 1, 0 ); -- Gold
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 675, 1, 1 ); -- Silver_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 671, 1, 0 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 616, 1, 0 ); -- Old_Card_Album
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 12516, 1, 0 ); -- E_Small_Life_Potion
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 22777, 1, 0 ); -- Gift_Buff_Set
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 4, 6231, 1, 0 ); -- Guarantee_Weapon_6Up
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 671, 1, 1 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 12246, 1, 0 ); -- Magic_Card_Album
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 12263, 1, 0 ); -- Comp_Battle_Manual
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 671, 1, 0 ); -- Potion_Box
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 5, 6235, 1, 0 ); -- Guarantee_Armor_6Up
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 6, 671, 1, 1 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 6, 12766, 1, 0 ); -- Reward_Job_BM25
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 6, 6234, 1, 0 ); -- Guarantee_Armor_7Up
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 6, 6233, 1, 0 ); -- Guarantee_Armor_8Up
+
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 7, 671, 1, 1 ); -- Gold_Coin
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 7, 6233, 1, 0 ); -- Guarantee_Armor_8Up
+INSERT INTO `db_roulette`(`level`, `item_id`, `amount`, `flag` ) VALUES ( 7, 6233, 1, 0 ); -- Guarantee_Armor_8Up	// KRO lists this twice

+ 1 - 0
sql-files/upgrades/upgrade_20150619_log.sql

@@ -0,0 +1 @@
+ALTER TABLE `picklog` MODIFY `type` enum('M','P','L','T','V','S','N','C','A','R','G','E','B','O','I','X','D','U','$','F','Y') NOT NULL default 'P',

+ 69 - 6
src/char/char.c

@@ -837,6 +837,54 @@ int char_inventory_to_sql(const struct item items[], int max, int id) {
 	return errors;
 }
 
+/**
+ * Returns the correct gender ID for the given character and enum value.
+ *
+ * If the per-character sex is defined but not supported by the current packetver, the database entries are corrected.
+ *
+ * @param sd Character data, if available.
+ * @param p  Character status.
+ * @param sex Character sex (database enum)
+ *
+ * @retval SEX_MALE if the per-character sex is male
+ * @retval SEX_FEMALE if the per-character sex is female
+ * @retval 99 if the per-character sex is not defined or the current PACKETVER doesn't support it.
+ */
+int char_mmo_gender(const struct char_session_data *sd, const struct mmo_charstatus *p, char sex)
+{
+#if PACKETVER >= 20141016
+	(void)sd; (void)p; // Unused
+	switch (sex) {
+		case 'M':
+			return SEX_MALE;
+		case 'F':
+			return SEX_FEMALE;
+		case 'U':
+		default:
+			return 99;
+	}
+#else
+	if (sex == 'M' || sex == 'F') {
+		if (!sd) {
+			// sd is not available, there isn't much we can do. Just return and print a warning.
+			ShowWarning("Character '%s' (CID: %d, AID: %d) has sex '%c', but PACKETVER does not support per-character sex. Defaulting to 'U'.\n",
+					p->name, p->char_id, p->account_id, sex);
+			return 99;
+		}
+		if ((sex == 'M' && sd->sex == SEX_FEMALE)
+		 || (sex == 'F' && sd->sex == SEX_MALE)) {
+			ShowWarning("Changing sex of character '%s' (CID: %d, AID: %d) to 'U' due to incompatible PACKETVER.\n", p->name, p->char_id, p->account_id);
+			chlogif_parse_ackchangecharsex(p->char_id, sd->sex);
+		} else {
+			ShowInfo("Resetting sex of character '%s' (CID: %d, AID: %d) to 'U' due to incompatible PACKETVER.\n", p->name, p->char_id, p->account_id);
+		}
+		if (SQL_ERROR == Sql_Query(sql_handle, "UPDATE `%s` SET `sex` = 'U' WHERE `char_id` = '%d'", schema_config.char_db, p->char_id)) {
+			Sql_ShowDebug(sql_handle);
+		}
+	}
+	return 99;
+#endif
+}
 
 int char_mmo_char_tobuf(uint8* buf, struct mmo_charstatus* p);
 
@@ -847,16 +895,16 @@ int char_mmo_chars_fromsql(struct char_session_data* sd, uint8* buf) {
 	struct mmo_charstatus p;
 	int j = 0, i;
 	char last_map[MAP_NAME_LENGTH_EXT];
+	char sex[2];
 
 	stmt = SqlStmt_Malloc(sql_handle);
-	if( stmt == NULL )
-	{
+	if( stmt == NULL ) {
 		SqlStmt_ShowDebug(stmt);
 		return 0;
 	}
 	memset(&p, 0, sizeof(p));
 
-	for( i = 0; i < MAX_CHARS; i++ ){
+	for( i = 0; i < MAX_CHARS; i++ ) {
 		sd->found_char[i] = -1;
 		sd->unban_time[i] = 0;
 	}
@@ -867,7 +915,7 @@ int char_mmo_chars_fromsql(struct char_session_data* sd, uint8* buf) {
 		"`str`,`agi`,`vit`,`int`,`dex`,`luk`,`max_hp`,`hp`,`max_sp`,`sp`,"
 		"`status_point`,`skill_point`,`option`,`karma`,`manner`,`hair`,`hair_color`,"
 		"`clothes_color`,`weapon`,`shield`,`head_top`,`head_mid`,`head_bottom`,`last_map`,`rename`,`delete_date`,"
-		"`robe`,`moves`,`unban_time`,`font`,`uniqueitem_counter`"
+		"`robe`,`moves`,`unban_time`,`font`,`uniqueitem_counter`,`sex`"
 		" FROM `%s` WHERE `account_id`='%d' AND `char_num` < '%d'", schema_config.char_db, sd->account_id, MAX_CHARS)
 	||	SQL_ERROR == SqlStmt_Execute(stmt)
 	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 0,  SQLDT_INT,    &p.char_id, 0, NULL, NULL)
@@ -910,6 +958,7 @@ int char_mmo_chars_fromsql(struct char_session_data* sd, uint8* buf) {
 	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 37, SQLDT_LONG,   &p.unban_time, 0, NULL, NULL)
 	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 38, SQLDT_UCHAR,  &p.font, 0, NULL, NULL)
 	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 39, SQLDT_UINT,   &p.uniqueitem_counter, 0, NULL, NULL)
+	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 40, SQLDT_ENUM,   &sex, sizeof(sex), NULL, NULL)
 	)
 	{
 		SqlStmt_ShowDebug(stmt);
@@ -922,6 +971,7 @@ int char_mmo_chars_fromsql(struct char_session_data* sd, uint8* buf) {
 		p.last_point.map = mapindex_name2id(last_map);
 		sd->found_char[p.slot] = p.char_id;
 		sd->unban_time[p.slot] = p.unban_time;
+		p.sex = char_mmo_gender(sd, &p, sex[0]);
 		j += char_mmo_char_tobuf(WBUFP(buf, j), &p);
 
 		// Addon System
@@ -954,6 +1004,7 @@ int char_mmo_char_fromsql(uint32 char_id, struct mmo_charstatus* p, bool load_ev
 	int hotkey_num;
 #endif
 	StringBuf msg_buf;
+	char sex[2];
 
 	memset(p, 0, sizeof(struct mmo_charstatus));
 
@@ -973,7 +1024,7 @@ int char_mmo_char_fromsql(uint32 char_id, struct mmo_charstatus* p, bool load_ev
 		"`status_point`,`skill_point`,`option`,`karma`,`manner`,`party_id`,`guild_id`,`pet_id`,`homun_id`,`elemental_id`,`hair`,"
 		"`hair_color`,`clothes_color`,`weapon`,`shield`,`head_top`,`head_mid`,`head_bottom`,`last_map`,`last_x`,`last_y`,"
 		"`save_map`,`save_x`,`save_y`,`partner_id`,`father`,`mother`,`child`,`fame`,`rename`,`delete_date`,`robe`, `moves`,"
-		"`unban_time`,`font`,`uniqueitem_counter`"
+		"`unban_time`,`font`,`uniqueitem_counter`,`sex`"
 		" FROM `%s` WHERE `char_id`=? LIMIT 1", schema_config.char_db)
 	||	SQL_ERROR == SqlStmt_BindParam(stmt, 0, SQLDT_INT, &char_id, 0)
 	||	SQL_ERROR == SqlStmt_Execute(stmt)
@@ -1033,6 +1084,7 @@ int char_mmo_char_fromsql(uint32 char_id, struct mmo_charstatus* p, bool load_ev
 	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 53, SQLDT_LONG,   &p->unban_time, 0, NULL, NULL)
 	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 54, SQLDT_UCHAR,  &p->font, 0, NULL, NULL)
 	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 55, SQLDT_UINT,   &p->uniqueitem_counter, 0, NULL, NULL)
+	||	SQL_ERROR == SqlStmt_BindColumn(stmt, 56, SQLDT_ENUM,   &sex, sizeof(sex), NULL, NULL)
 	)
 	{
 		SqlStmt_ShowDebug(stmt);
@@ -1045,6 +1097,7 @@ int char_mmo_char_fromsql(uint32 char_id, struct mmo_charstatus* p, bool load_ev
 		SqlStmt_Free(stmt);
 		return 0;
 	}
+	p->sex = char_mmo_gender(NULL, p, sex[0]);
 	p->last_point.map = mapindex_name2id(last_map);
 	p->save_point.map = mapindex_name2id(save_map);
 
@@ -1669,7 +1722,13 @@ int char_mmo_char_tobuf(uint8* buffer, struct mmo_charstatus* p)
 	WBUFW(buf,48) = min(p->max_sp, INT16_MAX);
 	WBUFW(buf,50) = DEFAULT_WALK_SPEED; // p->speed;
 	WBUFW(buf,52) = p->class_;
+#if PACKETVER >= 20141022
+	WBUFL(buf,54) = p->hair;
+	offset+=2;
+	buf = WBUFP(buffer,offset);
+#else
 	WBUFW(buf,54) = p->hair;
+#endif
 
 	//When the weapon is sent and your option is riding, the client crashes on login!?
 	WBUFW(buf,56) = p->option&(0x20|0x80000|0x100000|0x200000|0x400000|0x800000|0x1000000|0x2000000|0x4000000|0x8000000) ? 0 : p->weapon;
@@ -1723,6 +1782,10 @@ int char_mmo_char_tobuf(uint8* buffer, struct mmo_charstatus* p)
 		WBUFL(buf,136) = ( p->rename > 0 ) ? 1 : 0;  // (0 = disabled, otherwise displays "Add-Ons" sidebar)
 		offset += 4;
 	#endif
+	#if PACKETVER >= 20141016
+		WBUFB(buf,140) = p->sex;// sex - (0 = female, 1 = male, 99 = logindefined)
+		offset += 1;
+	#endif
 #endif
 
 	return 106+offset;
@@ -2890,7 +2953,7 @@ int do_init(int argc, char **argv)
 	add_timer_func_list(char_online_data_cleanup, "online_data_cleanup");
 	add_timer_interval(gettick() + 1000, char_online_data_cleanup, 0, 0, 600 * 1000);
 
-	//chek db tables
+	//check db tables
 	if(charserv_config.char_check_db && char_checkdb() == 0){
 		ShowFatalError("char : A tables is missing in sql-server, please fix it, see (sql-files main.sql for structure) \n");
 		exit(EXIT_FAILURE);

+ 2 - 1
src/char/char.h

@@ -219,7 +219,7 @@ extern struct fame_list chemist_fame_list[MAX_FAME_LIST];
 extern struct fame_list taekwon_fame_list[MAX_FAME_LIST];
 
 #define DEFAULT_AUTOSAVE_INTERVAL 300*1000
-#define MAX_CHAR_BUF 144 //Max size (for WFIFOHEAD calls)
+#define MAX_CHAR_BUF 150 //Max size (for WFIFOHEAD calls)
 
 int char_search_mapserver(unsigned short map, uint32 ip, uint16 port);
 int char_lan_subnetcheck(uint32 ip);
@@ -233,6 +233,7 @@ void char_set_all_offline(int id);
 void char_disconnect_player(uint32 account_id);
 int char_chardb_waiting_disconnect(int tid, unsigned int tick, int id, intptr_t data);
 
+int char_mmo_gender(const struct char_session_data *sd, const struct mmo_charstatus *p, char sex);
 int char_mmo_char_tobuf(uint8* buffer, struct mmo_charstatus* p);
 int char_mmo_char_tosql(uint32 char_id, struct mmo_charstatus* p);
 int char_mmo_char_fromsql(uint32 char_id, struct mmo_charstatus* p, bool load_everything);

+ 2 - 1
src/char/char_clif.c

@@ -711,7 +711,8 @@ int chclif_parse_charselect(int fd, struct char_session_data* sd,uint32 ipl){
 
 		//Have to switch over to the DB instance otherwise data won't propagate [Kevin]
 		cd = (struct mmo_charstatus *)idb_get(char_db_, char_id);
-		cd->sex = sd->sex;
+		if (cd->sex == 99)
+			cd->sex = sd->sex;
 
 		if (charserv_config.log_char) {
 			char esc_name[NAME_LENGTH*2+1];

+ 105 - 54
src/char/char_logif.c

@@ -340,76 +340,79 @@ int chlogif_parse_keepalive(int fd, struct char_session_data* sd){
 	return 1;
 }
 
-int chlogif_parse_ackchangesex(int fd, struct char_session_data* sd){
+/**
+ * Performs the necessary operations when changing a character's sex, such as
+ * correcting the job class and unequipping items, and propagating the
+ * information to the guild data.
+ *
+ * @param sex      The new sex (SEX_MALE or SEX_FEMALE).
+ * @param acc      The character's account ID.
+ * @param char_id  The character ID.
+ * @param class_   The character's current job class.
+ * @param guild_id The character's guild ID.
+ */
+void chlogif_parse_change_sex_sub(int sex, int acc, int char_id, int class_, int guild_id)
+{
+	// job modification
+	if (class_ == JOB_BARD || class_ == JOB_DANCER)
+		class_ = (sex == SEX_MALE ? JOB_BARD : JOB_DANCER);
+	else if (class_ == JOB_CLOWN || class_ == JOB_GYPSY)
+		class_ = (sex == SEX_MALE ? JOB_CLOWN : JOB_GYPSY);
+	else if (class_ == JOB_BABY_BARD || class_ == JOB_BABY_DANCER)
+		class_ = (sex == SEX_MALE ? JOB_BABY_BARD : JOB_BABY_DANCER);
+	else if (class_ == JOB_MINSTREL || class_ == JOB_WANDERER)
+		class_ = (sex == SEX_MALE ? JOB_MINSTREL : JOB_WANDERER);
+	else if (class_ == JOB_MINSTREL_T || class_ == JOB_WANDERER_T)
+		class_ = (sex == SEX_MALE ? JOB_MINSTREL_T : JOB_WANDERER_T);
+	else if (class_ == JOB_BABY_MINSTREL || class_ == JOB_BABY_WANDERER)
+		class_ = (sex == SEX_MALE ? JOB_BABY_MINSTREL : JOB_BABY_WANDERER);
+	else if (class_ == JOB_KAGEROU || class_ == JOB_OBORO)
+		class_ = (sex == SEX_MALE ? JOB_KAGEROU : JOB_OBORO);
+
+	if (SQL_ERROR == Sql_Query(sql_handle, "UPDATE `%s` SET `equip` = '0' WHERE `char_id` = '%d'", schema_config.inventory_db, char_id))
+		Sql_ShowDebug(sql_handle);
+
+	if (SQL_ERROR == Sql_Query(sql_handle, "UPDATE `%s` SET `class` = '%d', `weapon` = '0', `shield` = '0', `head_top` = '0', `head_mid` = '0', `head_bottom` = '0' WHERE `char_id` = '%d'", schema_config.char_db, class_, char_id))
+		Sql_ShowDebug(sql_handle);
+	if (guild_id) // If there is a guild, update the guild_member data [Skotlex]
+		inter_guild_sex_changed(guild_id, acc, char_id, sex);
+}
+
+int chlogif_parse_ackchangesex(int fd, struct char_session_data* sd)
+{
 	if (RFIFOREST(fd) < 7)
 		return 0;
 	{
 		unsigned char buf[7];
-
 		int acc = RFIFOL(fd,2);
 		int sex = RFIFOB(fd,6);
 		RFIFOSKIP(fd,7);
 
-		if( acc > 0 )
-		{// TODO: Is this even possible?
-			uint32 char_id[MAX_CHARS];
-			int class_[MAX_CHARS];
-			int guild_id[MAX_CHARS];
-			unsigned char num, i;
-			char* data;
-			DBMap*  auth_db = char_get_authdb();
-
+		if (acc > 0) { // TODO: Is this even possible?
+			unsigned char i;
+			int char_id = 0, class_ = 0, guild_id = 0;
+			DBMap* auth_db = char_get_authdb();
 			struct auth_node* node = (struct auth_node*)idb_get(auth_db, acc);
-			if( node != NULL )
+			SqlStmt *stmt;
+
+			if (node != NULL)
 				node->sex = sex;
 
 			// get characters
-			if( SQL_ERROR == Sql_Query(sql_handle, "SELECT `char_id`,`class`,`guild_id` FROM `%s` WHERE `account_id` = '%d'", schema_config.char_db, acc) )
-				Sql_ShowDebug(sql_handle);
-			for( i = 0; i < MAX_CHARS && SQL_SUCCESS == Sql_NextRow(sql_handle); ++i )
-			{
-				Sql_GetData(sql_handle, 0, &data, NULL); char_id[i] = atoi(data);
-				Sql_GetData(sql_handle, 1, &data, NULL); class_[i] = atoi(data);
-				Sql_GetData(sql_handle, 2, &data, NULL); guild_id[i] = atoi(data);
+			stmt = SqlStmt_Malloc(sql_handle);
+			if (SQL_ERROR == SqlStmt_Prepare(stmt, "SELECT `char_id`, `class`, `guild_id` FROM `%s` WHERE `account_id` = '%d'", schema_config.char_db, acc) || SqlStmt_Execute(stmt)) {
+				SqlStmt_ShowDebug(stmt);
+				SqlStmt_Free(stmt);
 			}
-			num = i;
-			for( i = 0; i < num; ++i )
-			{
-				if( class_[i] == JOB_BARD || class_[i] == JOB_DANCER ||
-					class_[i] == JOB_CLOWN || class_[i] == JOB_GYPSY ||
-					class_[i] == JOB_BABY_BARD || class_[i] == JOB_BABY_DANCER ||
-					class_[i] == JOB_MINSTREL || class_[i] == JOB_WANDERER ||
-					class_[i] == JOB_MINSTREL_T || class_[i] == JOB_WANDERER_T ||
-					class_[i] == JOB_BABY_MINSTREL || class_[i] == JOB_BABY_WANDERER ||
-					class_[i] == JOB_KAGEROU || class_[i] == JOB_OBORO )
-				{
-					// job modification
-					if( class_[i] == JOB_BARD || class_[i] == JOB_DANCER )
-						class_[i] = (sex ? JOB_BARD : JOB_DANCER);
-					else if( class_[i] == JOB_CLOWN || class_[i] == JOB_GYPSY )
-						class_[i] = (sex ? JOB_CLOWN : JOB_GYPSY);
-					else if( class_[i] == JOB_BABY_BARD || class_[i] == JOB_BABY_DANCER )
-						class_[i] = (sex ? JOB_BABY_BARD : JOB_BABY_DANCER);
-					else if( class_[i] == JOB_MINSTREL || class_[i] == JOB_WANDERER )
-						class_[i] = (sex ? JOB_MINSTREL : JOB_WANDERER);
-					else if( class_[i] == JOB_MINSTREL_T || class_[i] == JOB_WANDERER_T )
-						class_[i] = (sex ? JOB_MINSTREL_T : JOB_WANDERER_T);
-					else if( class_[i] == JOB_BABY_MINSTREL || class_[i] == JOB_BABY_WANDERER )
-						class_[i] = (sex ? JOB_BABY_MINSTREL : JOB_BABY_WANDERER);
-					else if( class_[i] == JOB_KAGEROU || class_[i] == JOB_OBORO )
-						class_[i] = (sex ? JOB_KAGEROU : JOB_OBORO);
-				}
 
-				if( SQL_ERROR == Sql_Query(sql_handle, "UPDATE `%s` SET `class`='%d', `weapon`='0', `shield`='0', `head_top`='0', `head_mid`='0', `head_bottom`='0' WHERE `char_id`='%d'", schema_config.char_db, class_[i], char_id[i]) )
-					Sql_ShowDebug(sql_handle);
+			SqlStmt_BindColumn(stmt, 0, SQLDT_INT,   &char_id,  0, NULL, NULL);
+			SqlStmt_BindColumn(stmt, 1, SQLDT_SHORT, &class_,   0, NULL, NULL);
+			SqlStmt_BindColumn(stmt, 2, SQLDT_INT,   &guild_id, 0, NULL, NULL);
 
-				if( guild_id[i] )// If there is a guild, update the guild_member data [Skotlex]
-					inter_guild_sex_changed(guild_id[i], acc, char_id[i], sex);
+			for (i = 0; i < MAX_CHARS && SQL_SUCCESS == SqlStmt_NextRow(stmt); ++i) {
+				chlogif_parse_change_sex_sub(sex, acc, char_id, class_, guild_id);
 			}
-			Sql_FreeResult(sql_handle);
-
-			// disconnect player if online on char-server
-			char_disconnect_player(acc);
+			SqlStmt_Free(stmt);
 		}
 
 		// notify all mapservers about this change
@@ -421,6 +424,54 @@ int chlogif_parse_ackchangesex(int fd, struct char_session_data* sd){
 	return 1;
 }
 
+/**
+ * Changes a character's sex.
+ * The information is updated on database, and the character is kicked if it
+ * currently is online.
+ *
+ * @param char_id The character's ID.
+ * @param sex     The new sex.
+ * @retval 0 in case of success.
+ * @retval 1 in case of failure.
+ */
+int chlogif_parse_ackchangecharsex(int char_id, int sex)
+{
+	int class_ = 0, guild_id = 0, account_id = 0;
+	unsigned char buf[7];
+	char *data;
+
+	// get character data
+	if (SQL_ERROR == Sql_Query(sql_handle, "SELECT `account_id`,`class`,`guild_id` FROM `%s` WHERE `char_id` = '%d'", schema_config.char_db, char_id)) {
+		Sql_ShowDebug(sql_handle);
+		return 1;
+	}
+	if (Sql_NumRows(sql_handle) != 1 || SQL_ERROR == Sql_NextRow(sql_handle)) {
+		Sql_FreeResult(sql_handle);
+		return 1;
+	}
+
+	Sql_GetData(sql_handle, 0, &data, NULL); account_id = atoi(data);
+	Sql_GetData(sql_handle, 1, &data, NULL); class_ = atoi(data);
+	Sql_GetData(sql_handle, 2, &data, NULL); guild_id = atoi(data);
+	Sql_FreeResult(sql_handle);
+
+	if (SQL_ERROR == Sql_Query(sql_handle, "UPDATE `%s` SET `sex` = '%c' WHERE `char_id` = '%d'", schema_config.char_db, sex == SEX_MALE ? 'M' : 'F', char_id)) {
+		Sql_ShowDebug(sql_handle);
+		return 1;
+	}
+	chlogif_parse_change_sex_sub(sex, account_id, char_id, class_, guild_id);
+
+	// disconnect player if online on char-server
+	char_disconnect_player(account_id);
+
+	// notify all mapservers about this change
+	WBUFW(buf,0) = 0x2b0d;
+	WBUFL(buf,2) = account_id;
+	WBUFB(buf,6) = sex;
+	chmapif_sendall(buf, 7);
+	return 0;
+}
+
 int chlogif_parse_ackacc2req(int fd, struct char_session_data* sd){
 	if (RFIFOREST(fd) < 4 || RFIFOREST(fd) < RFIFOW(fd,2))
 		return 0;

+ 2 - 0
src/char/char_logif.h

@@ -30,7 +30,9 @@ int chlogif_parse_ackconnect(int fd, struct char_session_data* sd);
 int chlogif_parse_ackaccreq(int fd, struct char_session_data* sd);
 int chlogif_parse_reqaccdata(int fd, struct char_session_data* sd);
 int chlogif_parse_keepalive(int fd, struct char_session_data* sd);
+void chlogif_parse_change_sex_sub(int sex, int acc, int char_id, int class_, int guild_id);
 int chlogif_parse_ackchangesex(int fd, struct char_session_data* sd);
+int chlogif_parse_ackchangecharsex(int char_id, int sex);
 int chlogif_parse_ackacc2req(int fd, struct char_session_data* sd);
 int chlogif_parse_accbannotification(int fd, struct char_session_data* sd);
 int chlogif_parse_askkick(int fd, struct char_session_data* sd);

+ 27 - 11
src/char/char_mapif.c

@@ -717,7 +717,7 @@ int chmapif_parse_reqnewemail(int fd){
 
 /**
  * Forward a change of status for account to login-serv
- * @param fd: wich fd to parse from
+ * @param fd: which fd to parse from
  * @return : 0 not enough data received, 1 success
  */
 int chmapif_parse_fwlog_changestatus(int fd){
@@ -730,14 +730,19 @@ int chmapif_parse_fwlog_changestatus(int fd){
 
 		int aid = RFIFOL(fd,2); // account_id of who ask (-1 if server itself made this request)
 		const char* name = (char*)RFIFOP(fd,6); // name of the target character
-		int operation = RFIFOW(fd,30); // type of operation: 1-block, 2-ban, 3-unblock, 4-unban, 5-changesex, 6-vip
-		int32 timediff = RFIFOL(fd,32);
-		int val1 = RFIFOL(fd,36);
-		//int val2 = RFIFOL(fd,40); // Since BankVault is moved out, this value is unused for now
+		int operation = RFIFOW(fd,30); // type of operation: 1-block, 2-ban, 3-unblock, 4-unban, 5-changesex, 6-vip, 7-changecharsex
+		int32 timediff = 0;
+		int val1 = 0, sex = SEX_MALE;
+
+		if (operation == 2) {
+			timediff = RFIFOL(fd, 32);
+			val1 = RFIFOL(fd, 36);
+		} else if (operation == 7)
+			sex = RFIFOB(fd, 32);
 		RFIFOSKIP(fd,44);
 
 		Sql_EscapeStringLen(sql_handle, esc_name, name, strnlen(name, NAME_LENGTH));
-		if( SQL_ERROR == Sql_Query(sql_handle, "SELECT `account_id` FROM `%s` WHERE `name` = '%s'", schema_config.char_db, esc_name) )
+		if( SQL_ERROR == Sql_Query(sql_handle, "SELECT `account_id`, `char_id` FROM `%s` WHERE `name` = '%s'", schema_config.char_db, esc_name) )
 			Sql_ShowDebug(sql_handle);
 		else if( Sql_NumRows(sql_handle) == 0 ) {
 			result = 1; // 1-player not found
@@ -746,10 +751,12 @@ int chmapif_parse_fwlog_changestatus(int fd){
 			Sql_ShowDebug(sql_handle);
 			result = 1;
 		} else {
-			int t_aid; //targit account id
+			int t_aid; // target account id
+			int t_cid; // target char id
 			char* data;
 
 			Sql_GetData(sql_handle, 0, &data, NULL); t_aid = atoi(data);
+			Sql_GetData(sql_handle, 0, &data, NULL); t_cid = atoi(data);
 			Sql_FreeResult(sql_handle);
 
 			if(!chlogif_isconnected())
@@ -794,16 +801,24 @@ int chmapif_parse_fwlog_changestatus(int fd){
 						WFIFOL(login_fd,2) = t_aid;
 						WFIFOSET(login_fd,6);
 						break;
-					case 6:
+					case 6: // vip
 						answer = (val1&4); // vip_req val1=type, &1 login send return, &2 update timestamp, &4 map send answer
 						chlogif_reqvipdata(t_aid, val1, timediff, fd);
 						break;
+					case 7: // changecharsex
+						answer = false;
+						WFIFOHEAD(login_fd,6);
+						WFIFOW(login_fd,0) = 0x2727;
+						WFIFOL(login_fd,2) = t_aid;
+						WFIFOL(login_fd,6) = t_cid;
+						WFIFOSET(login_fd,10);
+						break;
 				} //end switch operation
 			} //login is connected
 		}
 
 		// send answer if a player asks, not if the server asks
-		if( aid != -1 && answer) { // Don't send answer for changesex
+		if( aid != -1 && answer) { // Don't send answer for changesex/changecharsex
 			WFIFOHEAD(fd,34);
 			WFIFOW(fd, 0) = 0x2b0f;
 			WFIFOL(fd, 2) = aid;
@@ -817,7 +832,7 @@ int chmapif_parse_fwlog_changestatus(int fd){
 }
 
 /**
- * Transmit the acknolegement of divorce of partner_id1 and partner_id2
+ * Transmit the acknowledgement of divorce of partner_id1 and partner_id2
  * Update the list associated and transmit the new ranking
  * @param partner_id1: char id1 divorced
  * @param partner_id2: char id2 divorced
@@ -1047,7 +1062,8 @@ int chmapif_parse_reqauth(int fd, int id){
             node->ip == ip*/ )
         {// auth ok
             uint16 mmo_charstatus_len = sizeof(struct mmo_charstatus) + 25;
-            cd->sex = sex;
+			if (cd->sex == 99)
+				cd->sex = sex;
 
             WFIFOHEAD(fd,mmo_charstatus_len);
             WFIFOW(fd,0) = 0x2afd;

+ 5 - 1
src/common/utils.c

@@ -378,7 +378,11 @@ uint32 date2version(int date) {
 	else if(date < 20130717) return 43;
 	else if(date < 20130807) return 44;
 	else if(date < 20131223) return 45;
-	else if(date >= 20131223) return 46;
+	else if(date < 20140212) return 46;
+	else if(date < 20140613) return 47;
+	else if(date < 20141016) return 48;
+	else if(date < 20141022) return 49;
+	else if(date >= 20141022) return 50;
 
 	else return 30; //default
 }

+ 31 - 4
src/map/atcommand.c

@@ -6769,17 +6769,43 @@ ACMD_FUNC(uptime)
 
 /*==========================================
  * @changesex <sex>
- * => Changes one's sex. Argument sex can be 0 or 1, m or f, male or female.
+ * => Changes one's account sex. Argument sex can be 0 or 1, m or f, male or female.
  *------------------------------------------*/
 ACMD_FUNC(changesex)
 {
 	int i;
+
+	nullpo_retr(-1, sd);
+
+	pc_resetskill(sd,4);
+	// to avoid any problem with equipment and invalid sex, equipment is unequiped.
+	for (i = 0; i < EQI_MAX; i++) {
+		if (sd->equip_index[i] >= 0)
+			pc_unequipitem(sd, sd->equip_index[i], 3);
+	}
+
+	chrif_changesex(sd, true);
+	return 0;
+}
+
+/*==========================================
+ * @changecharsex <sex>
+ * => Changes one's character sex. Argument sex can be 0 or 1, m or f, male or female.
+ *------------------------------------------*/
+ACMD_FUNC(changecharsex)
+{
+	int i;
+
 	nullpo_retr(-1, sd);
+
 	pc_resetskill(sd,4);
 	// to avoid any problem with equipment and invalid sex, equipment is unequiped.
-	for( i=0; i<EQI_MAX; i++ )
-		if( sd->equip_index[i] >= 0 ) pc_unequipitem(sd, sd->equip_index[i], 3);
-	chrif_changesex(sd);
+	for (i = 0; i < EQI_MAX; i++) {
+		if (sd->equip_index[i] >= 0)
+			pc_unequipitem(sd, sd->equip_index[i], 3);
+	}
+
+	chrif_changesex(sd, false);
 	return 0;
 }
 
@@ -9886,6 +9912,7 @@ void atcommand_basecommands(void) {
 		ACMD_DEF(clearweather),
 		ACMD_DEF(uptime),
 		ACMD_DEF(changesex),
+		ACMD_DEF(changecharsex),
 		ACMD_DEF(mute),
 		ACMD_DEF(refresh),
 		ACMD_DEF(refreshall),

+ 8 - 0
src/map/battle.c

@@ -8168,6 +8168,7 @@ static const struct _battle_data {
 	{ "homunculus_evo_intimacy_need",       &battle_config.homunculus_evo_intimacy_need,    91100,  0,      INT_MAX,        },
 	{ "homunculus_evo_intimacy_reset",      &battle_config.homunculus_evo_intimacy_reset,   1000,   0,      INT_MAX,        },
 	{ "monster_loot_search_type",           &battle_config.monster_loot_search_type,        1,      0,      1,              },
+	{ "feature.roulette",                   &battle_config.feature_roulette,                1,      0,      1,              },
 };
 
 #ifndef STATS_OPT_OUT
@@ -8396,6 +8397,13 @@ void battle_adjust_conf()
 	}
 #endif
 
+#if PACKETVER < 20141022
+	if (battle_config.feature_roulette) {
+		ShowWarning("conf/battle/feature.conf roulette is enabled but it requires PACKETVER 2014-10-22 or newer, disabling...\n");
+		battle_config.feature_roulette = 0;
+	}
+#endif
+
 #ifndef CELL_NOSTACK
 	if (battle_config.custom_cell_stack_limit != 1)
 		ShowWarning("Battle setting 'custom_cell_stack_limit' takes no effect as this server was compiled without Cell Stack Limit support.\n");

+ 1 - 0
src/map/battle.h

@@ -590,6 +590,7 @@ extern struct Battle_Config
 	int homunculus_evo_intimacy_need;
 	int homunculus_evo_intimacy_reset;
 	int monster_loot_search_type;
+	int feature_roulette;
 } battle_config;
 
 void do_init_battle(void);

+ 10 - 5
src/map/chrif.c

@@ -63,7 +63,7 @@ static const int packet_len_table[0x3d] = { // U - used, F - free
 //2b0a: Outgoing, chrif_skillcooldown_request -> requesting the list of skillcooldown for char
 //2b0b: Incoming, chrif_skillcooldown_load -> received the list of cooldown for char
 //2b0c: Outgoing, chrif_changeemail -> 'change mail address ...'
-//2b0d: Incoming, chrif_changedsex -> 'Change sex of acc XY'
+//2b0d: Incoming, chrif_changedsex -> 'Change sex of acc XY' (or char)
 //2b0e: Outgoing, chrif_req_login_operation -> 'Do some operations (change sex, ban / unban etc)'
 //2b0f: Incoming, chrif_ack_login_req -> 'answer of the 2b0e'
 //2b10: Outgoing, chrif_updatefamelist -> 'Update the fame ranking lists and send them'
@@ -864,17 +864,19 @@ int chrif_req_login_operation(int aid, const char* character_name, unsigned shor
 
 /**
  * S 2b0e <accid>.l <name>.24B <operation_type>.w <timediff>L <val1>L <val2>L
- * Send an account modification (changesex) request to the login server (via char server).
+ * Send a sex change (for account or character) request to the login server (via char server).
  * @sd : Player requesting operation
  */
-int chrif_changesex(struct map_session_data *sd) {
+int chrif_changesex(struct map_session_data *sd, bool change_account) {
 	chrif_check(-1);
 
 	WFIFOHEAD(char_fd,44);
 	WFIFOW(char_fd,0) = 0x2b0e;
 	WFIFOL(char_fd,2) = sd->status.account_id;
 	safestrncpy((char*)WFIFOP(char_fd,6), sd->status.name, NAME_LENGTH);
-	WFIFOW(char_fd,30) = CHRIF_OP_LOGIN_CHANGESEX;
+	WFIFOW(char_fd,30) = (change_account ? CHRIF_OP_LOGIN_CHANGESEX : CHRIF_OP_LOGIN_CHANGECHARSEX);
+	if (!change_account)
+		WFIFOB(char_fd,32) = sd->status.sex == SEX_MALE ? SEX_FEMALE : SEX_MALE;
 	WFIFOSET(char_fd,44);
 
 	clif_displaymessage(sd->fd, msg_txt(sd,408)); //"Need disconnection to perform change-sex request..."
@@ -917,6 +919,9 @@ static void chrif_ack_login_req(int aid, const char* player_name, uint16 type, u
 		case CHRIF_OP_LOGIN_UNBLOCK:
 		case CHRIF_OP_LOGIN_UNBAN:
 		case CHRIF_OP_LOGIN_CHANGESEX:
+		case CHRIF_OP_LOGIN_CHANGECHARSEX:
+			if (type == CHRIF_OP_LOGIN_CHANGECHARSEX)
+				type--; // So we don't have to create a new msgstring.
 			snprintf(action,25,"%s",msg_txt(sd,427+type)); //block|ban|unblock|unban|change the sex of
 			break;
 		case CHRIF_OP_LOGIN_VIP:
@@ -929,7 +934,7 @@ static void chrif_ack_login_req(int aid, const char* player_name, uint16 type, u
 			break;
 	}
 
-	switch( answer ) {
+	switch (answer) {
 		case 0: sprintf(output, msg_txt(sd,424), action, NAME_LENGTH, player_name); break; //Login-serv has been asked to %s '%.*s'.
 		case 1: sprintf(output, msg_txt(sd,425), NAME_LENGTH, player_name); break;
 		case 2: sprintf(output, msg_txt(sd,426), action, NAME_LENGTH, player_name); break;

+ 2 - 1
src/map/chrif.h

@@ -27,6 +27,7 @@ enum chrif_req_op {
 	CHRIF_OP_LOGIN_UNBAN,
 	CHRIF_OP_LOGIN_CHANGESEX,
 	CHRIF_OP_LOGIN_VIP,
+	CHRIF_OP_LOGIN_CHANGECHARSEX,
 
 	// Char-server operation
 	CHRIF_OP_BAN,
@@ -73,7 +74,7 @@ int chrif_char_offline_nsd(uint32 account_id, uint32 char_id);
 int chrif_char_reset_offline(void);
 int send_users_tochar(void);
 int chrif_char_online(struct map_session_data *sd);
-int chrif_changesex(struct map_session_data *sd);
+int chrif_changesex(struct map_session_data *sd, bool change_account);
 int chrif_chardisconnect(struct map_session_data *sd);
 int chrif_divorce(int partner_id1, int partner_id2);
 

+ 268 - 8
src/map/clif.c

@@ -662,6 +662,9 @@ void clif_authok(struct map_session_data *sd)
 	WFIFOB(fd,10) = 5; // ignored
 #if PACKETVER >= 20080102
 	WFIFOW(fd,11) = sd->status.font;
+#endif
+#if PACKETVER >= 20141016
+	WFIFOB(fd,13) = sd->status.sex;
 #endif
 	WFIFOSET(fd,packet_len(cmd));
 }
@@ -5751,6 +5754,11 @@ void clif_displaymessage(const int fd, const char* mes)
 	if (fd == 0)
 		;
 	else {
+#if PACKETVER == 20141022
+		/** for some reason game client crashes depending on message pattern (only for this packet) **/
+		/** so we redirect to ZC_NPC_CHAT **/
+		clif_colormes(fd, color_table[COLOR_DEFAULT], mes);
+#else
 		char *message, *line;
 
 		message = aStrdup(mes);
@@ -5769,6 +5777,7 @@ void clif_displaymessage(const int fd, const char* mes)
 			line = strtok(NULL, "\n");
 		}
 		aFree(message);
+#endif
 	}
 }
 
@@ -6833,6 +6842,14 @@ void clif_openvending(struct map_session_data* sd, int id, struct s_vending* ven
 		clif_addcards(WFIFOP(fd,22+i*22), &sd->status.cart[index]);
 	}
 	WFIFOSET(fd,WFIFOW(fd,2));
+
+#if PACKETVER >= 20141022
+	// Should go elsewhere perhaps? It has to be bundled with this however.
+	WFIFOHEAD(fd, 3);
+	WFIFOW(fd, 0) = 0xa28;
+	WFIFOB(fd, 2) = 0; // 1 is failure. Our current responses to failure are working so not yet implemented.
+	WFIFOSET(fd, 3);
+#endif
 }
 
 
@@ -10114,18 +10131,22 @@ void clif_hotkeys_send(struct map_session_data *sd) {
 #ifdef HOTKEY_SAVING
 	const int fd = sd->fd;
 	int i;
+	int offset = 2;
 #if PACKETVER < 20090603
 	const int cmd = 0x2b9;
-#else
+#elif PACKETVER < 20141022
 	const int cmd = 0x7d9;
+#else
+	const int cmd = 0xa00;
+	offset = 3;
 #endif
 	if (!fd) return;
-	WFIFOHEAD(fd, 2+MAX_HOTKEYS*7);
+	WFIFOHEAD(fd, offset + MAX_HOTKEYS * 7);
 	WFIFOW(fd, 0) = cmd;
 	for(i = 0; i < MAX_HOTKEYS; i++) {
-		WFIFOB(fd, 2 + 0 + i * 7) = sd->status.hotkeys[i].type; // type: 0: item, 1: skill
-		WFIFOL(fd, 2 + 1 + i * 7) = sd->status.hotkeys[i].id; // item or skill ID
-		WFIFOW(fd, 2 + 5 + i * 7) = sd->status.hotkeys[i].lv; // skill level
+		WFIFOB(fd, offset + 0 + i * 7) = sd->status.hotkeys[i].type; // type: 0: item, 1: skill
+		WFIFOL(fd, offset + 1 + i * 7) = sd->status.hotkeys[i].id; // item or skill ID
+		WFIFOW(fd, offset + 5 + i * 7) = sd->status.hotkeys[i].lv; // item qty or skill level
 	}
 	WFIFOSET(fd, packet_len(cmd));
 #endif
@@ -17729,6 +17750,229 @@ void DumpUnknown(int fd,TBL_PC *sd,int cmd,int packet_len)
 }
 #endif
 
+/// Roulette System
+/// Author: Yommy
+
+/**
+ * Opens Roulette window
+ * @param fd
+ * @param sd
+ */
+void clif_parse_RouletteOpen(int fd, struct map_session_data* sd)
+{
+	nullpo_retv(sd);
+
+	if (!battle_config.feature_roulette) {
+		clif_colormes(sd,color_table[COLOR_RED],msg_txt(sd,1497)); //Roulette is disabled
+		return;
+	}
+
+	WFIFOHEAD(fd,packet_len(0xa1a));
+	WBUFW(fd,0) = 0xa1a;
+	WBUFW(fd,2) = 0; // result
+	WBUFL(fd,3) = 0; // serial
+	WBUFW(fd,7) = sd->roulette.stage - 1;
+	WBUFW(fd,8) = (char)sd->roulette.prizeIdx;
+	WBUFW(fd,9) = -1; // TODO
+	WBUFL(fd,11) = sd->roulette_point.gold;
+	WBUFL(fd,15) = sd->roulette_point.silver;
+	WBUFL(fd,19) = sd->roulette_point.bronze;
+	WFIFOSET(fd,packet_len(0xa1a));
+}
+
+/**
+ * Generates information to be displayed
+ * @param fd
+ * @param sd
+ */
+void clif_parse_RouletteInfo(int fd, struct map_session_data* sd)
+{
+	unsigned short i, j, count = 0;
+	int len = 8 + (42 * 8);
+
+	nullpo_retv(sd);
+
+	if (!battle_config.feature_roulette) {
+		clif_colormes(sd,color_table[COLOR_RED],msg_txt(sd,1497)); //Roulette is disabled
+		return;
+	}
+
+	WFIFOHEAD(fd,len);
+	WBUFW(fd,0) = 0xa1c;
+	WBUFW(fd,2) = len;
+	WBUFL(fd,6) = 1; // serial
+
+	for(i = 0; i < MAX_ROULETTE_LEVEL; i++) {
+		for(j = 0; j < MAX_ROULETTE_COLUMNS - i; j++) {
+			WBUFW(fd,8 + i * 8) = i;
+			WBUFW(fd,8 + i * 8 + 2) = j;
+			WBUFW(fd,8 + i * 8 + 4) = rd.nameid[i][j];
+			WBUFW(fd,8 + i * 8 + 6) = rd.qty[i][j];
+			count++;
+		}
+	}
+
+	WFIFOSET(fd,len);
+	return;
+}
+
+/**
+ * Closes Roulette window
+ * @param fd
+ * @param sd
+ */
+void clif_parse_RouletteClose(int fd, struct map_session_data* sd)
+{
+	nullpo_retv(sd);
+
+	if (!battle_config.feature_roulette) {
+		clif_colormes(sd,color_table[COLOR_RED],msg_txt(sd,1497)); //Roulette is disabled
+		return;
+	}
+
+	/** What do we need this for? (other than state tracking), game client closes the window without our response. **/
+	return;
+}
+
+/**
+ * Process the stage and attempt to give a prize
+ * @param fd
+ * @param sd
+ */
+void clif_parse_RouletteGenerate(int fd, struct map_session_data* sd)
+{
+	unsigned char result = GENERATE_ROULETTE_SUCCESS;
+	short stage = sd->roulette.stage;
+
+	nullpo_retv(sd);
+
+	if (!battle_config.feature_roulette) {
+		clif_colormes(sd,color_table[COLOR_RED],msg_txt(sd,1497)); //Roulette is disabled
+		return;
+	}
+
+	if (sd->roulette.stage >= MAX_ROULETTE_LEVEL)
+		stage = sd->roulette.stage = 0;
+
+	if (!stage) {
+		if (sd->roulette_point.bronze <= 0 && sd->roulette_point.silver < 10 && sd->roulette_point.gold < 10)
+			result = GENERATE_ROULETTE_NO_ENOUGH_POINT;
+	}
+
+	if (result == GENERATE_ROULETTE_SUCCESS) {
+		if (!stage) {
+			if (sd->roulette_point.bronze > 0) {
+				sd->roulette_point.bronze -= 1;
+			} else if (sd->roulette_point.silver > 9) {
+				sd->roulette_point.silver -= 10;
+				stage = sd->roulette.stage = 2;
+			} else if (sd->roulette_point.gold > 9) {
+				sd->roulette_point.gold -= 10;
+				stage = sd->roulette.stage = 4;
+			}
+		}
+
+		sd->roulette.prizeStage = stage;
+		sd->roulette.prizeIdx = rnd()%rd.items[stage];
+		if (sd->roulette.prizeIdx == 0) {
+			struct item it;
+			memset(&it, 0, sizeof(it));
+
+			it.nameid = rd.nameid[stage][0];
+			it.identify = 1;
+
+			pc_additem(sd, &it, rd.qty[stage][0], LOG_TYPE_ROULETTE);
+
+			sd->roulette.stage = 0;
+			result = GENERATE_ROULETTE_LOSING;
+		} else
+			sd->roulette.claimPrize = true;
+	}
+
+	clif_roulette_generate_ack(sd,result,stage,sd->roulette.prizeIdx,0);
+	if (result == GENERATE_ROULETTE_SUCCESS)
+		sd->roulette.stage++;
+}
+
+/**
+ * Request to cash in prize
+ * @param fd
+ * @param sd
+ */
+void clif_parse_RouletteRecvItem(int fd, struct map_session_data* sd)
+{
+	nullpo_retv(sd);
+
+	if (!battle_config.feature_roulette) {
+		clif_colormes(sd,color_table[COLOR_RED],msg_txt(sd,1497)); //Roulette is disabled
+		return;
+	}
+
+	WFIFOHEAD(fd,packet_len(0xa22));
+	WBUFW(fd,0) = 0xa22;
+
+	if (sd->roulette.claimPrize) {
+		struct item it;
+		memset(&it, 0, sizeof(it));
+
+		it.nameid = rd.nameid[sd->roulette.prizeStage][sd->roulette.prizeIdx];
+		it.identify = 1;
+
+		switch (pc_additem(sd, &it, rd.qty[sd->roulette.prizeStage][sd->roulette.prizeIdx], LOG_TYPE_OTHER)) {
+			case 0:
+				WBUFW(fd,2) = RECV_ITEM_SUCCESS;
+				sd->roulette.claimPrize = false;
+				sd->roulette.prizeStage = 0;
+				sd->roulette.prizeIdx = 0;
+				sd->roulette.stage = 0;
+				break;
+			case 1:
+			case 4:
+			case 5:
+				WBUFW(fd,2) = RECV_ITEM_OVERCOUNT;
+				break;
+			case 2:
+				WBUFW(fd,2) = RECV_ITEM_OVERWEIGHT;
+				break;
+			default:
+			case 7:
+				WBUFW(fd,2) = RECV_ITEM_FAILED;
+				break;
+		}
+	} else
+		WBUFW(fd,2) = RECV_ITEM_FAILED;
+
+	WBUFL(fd,3) = 0; // additional item TODO
+	WFIFOSET(fd,packet_len(0xa22));
+	return;
+}
+
+/**
+ * Update Roulette window with current stats
+ * @param sd
+ * @param result
+ * @param stage
+ * @param prizeIdx
+ * @param bonusItemID
+ */
+void clif_roulette_generate_ack(struct map_session_data *sd, unsigned char result, short stage, short prizeIdx, short bonusItemID)
+{
+	int fd = sd->fd;
+
+	nullpo_retv(sd);
+
+	WFIFOHEAD(fd,packet_len(0xa20));
+	WBUFW(fd,0) = 0xa20;
+	WBUFW(fd,2) = result;
+	WBUFW(fd,3) = stage;
+	WBUFW(fd,5) = prizeIdx;
+	WBUFW(fd,7) = bonusItemID;
+	WBUFL(fd,9) = sd->roulette_point.gold;
+	WBUFL(fd,13) = sd->roulette_point.silver;
+	WBUFL(fd,17) = sd->roulette_point.bronze;
+	WFIFOSET(fd,packet_len(0xa20));
+}
+
 /*==========================================
  * Main client packet processing function
  *------------------------------------------*/
@@ -18120,8 +18364,17 @@ void packetdb_readdb(bool reload)
 	//#0x09C0
 		0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 23,  0,  0,  0,102,  0,
 		0,  0,  0,  0,  2,  0, -1, -1,  2,  0,  0,  0,  0,  0,  0,  7,
-		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
-		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
+		0,  0,  0,  0,  0,  0,  0,  3, 11,  0, 11, -1,  0,  3, 11,  0,
+		0, 11, 12, 11,  0,  0,  0,  0,  0,143,  0,  0,  0,  0,  0,  0,
+	//#0x0a00
+#if PACKETVER >= 20141022
+	  269,  0,  0,  2,  6, 49,  6,  9, 26, 45, 47, 47, 56, -1,  0,  0,
+#else
+	  269,  0,  0,  2,  6, 48,  6,  9, 26, 45, 47, 47, 56, -1,  0,  0,
+#endif
+		0,  0,  0, 26,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
+		0,  3,  5,  0, 66,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
+ 		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
 	};
 	struct {
 		void (*func)(int, struct map_session_data *);
@@ -18344,9 +18597,15 @@ void packetdb_readdb(bool reload)
 		{ clif_parse_client_version, "clientversion"},
 		{ clif_parse_blocking_playcancel, "booking_playcancel"},
 		{ clif_parse_ranklist, "ranklist"},
-		/* Market NPC */
+		// Market NPC
 		{ clif_parse_NPCMarketClosed, "npcmarketclosed" },
 		{ clif_parse_NPCMarketPurchase, "npcmarketpurchase" },
+		// Roulette
+		{ clif_parse_RouletteOpen, "rouletteopen" },
+		{ clif_parse_RouletteInfo, "rouletteinfo" },
+		{ clif_parse_RouletteClose, "rouletteclose" },
+		{ clif_parse_RouletteGenerate, "roulettegenerate" },
+		{ clif_parse_RouletteRecvItem, "rouletterecvitem" },
 		{NULL,NULL}
 	};
 	struct {
@@ -18591,6 +18850,7 @@ void packetdb_readdb(bool reload)
  *------------------------------------------*/
 void do_init_clif(void) {
 	const char* colors[COLOR_MAX] = {
+		"0x00FF00",
 		"0xFF0000",
 		"0xFFFFFF",
 		"0xFFFF00",

+ 42 - 4
src/map/clif.h

@@ -33,9 +33,9 @@ struct party_booking_ad_info;
 #include <stdarg.h>
 
 enum { // packet DB
-	MIN_PACKET_DB  = 0x0064,
-	MAX_PACKET_DB  = 0xf00,
-	MAX_PACKET_VER = 46,
+	MIN_PACKET_DB  = 0x064,
+	MAX_PACKET_DB  = 0xAFF,
+	MAX_PACKET_VER = 51,
 	MAX_PACKET_POS = 20,
 };
 
@@ -73,13 +73,42 @@ enum e_BANKING_DEPOSIT_ACK {
 	BDA_NO_MONEY = 0x2,
 	BDA_OVERFLOW = 0x3,
 };
- 
+
 enum e_BANKING_WITHDRAW_ACK {
 	BWA_SUCCESS       = 0x0,
 	BWA_NO_MONEY      = 0x1,
 	BWA_UNKNOWN_ERROR = 0x2,
 };
 
+enum RECV_ROULETTE_ITEM_REQ {
+	RECV_ITEM_SUCCESS    = 0x0,
+	RECV_ITEM_FAILED     = 0x1,
+	RECV_ITEM_OVERCOUNT  = 0x2,
+	RECV_ITEM_OVERWEIGHT = 0x3,
+};
+
+enum RECV_ROULETTE_ITEM_ACK {
+	RECV_ITEM_NORMAL =  0x0,
+	RECV_ITEM_LOSING =  0x1,
+};
+
+enum GENERATE_ROULETTE_ACK {
+	GENERATE_ROULETTE_SUCCESS         = 0x0,
+	GENERATE_ROULETTE_FAILED          = 0x1,
+	GENERATE_ROULETTE_NO_ENOUGH_POINT = 0x2,
+	GENERATE_ROULETTE_LOSING          = 0x3,
+};
+
+enum OPEN_ROULETTE_ACK {
+	OPEN_ROULETTE_SUCCESS = 0x0,
+	OPEN_ROULETTE_FAILED  = 0x1,
+};
+
+enum CLOSE_ROULETTE_ACK {
+	CLOSE_ROULETTE_SUCCESS = 0x0,
+	CLOSE_ROULETTE_FAILED  = 0x1,
+};
+
 // packet_db[SERVER] is reserved for server use
 #define SERVER 0
 #define packet_len(cmd) packet_db[SERVER][cmd].len
@@ -861,6 +890,14 @@ void clif_cashshop_open( struct map_session_data* sd );
 
 void clif_display_pinfo(struct map_session_data *sd, int type);
 
+/// Roulette
+void clif_roulette_generate_ack(struct map_session_data *sd, unsigned char result, short stage, short prizeIdx, short bonusItemID);
+void clif_parse_RouletteOpen(int fd, struct map_session_data *sd);
+void clif_parse_RouletteInfo(int fd, struct map_session_data *sd);
+void clif_parse_RouletteClose(int fd, struct map_session_data *sd);
+void clif_parse_RouletteGenerate(int fd, struct map_session_data *sd);
+void clif_parse_RouletteRecvItem(int fd, struct map_session_data *sd);
+
 /**
  * 3CeAM
  **/
@@ -892,6 +929,7 @@ void clif_monster_hp_bar( struct mob_data* md, int fd );
  * Color Table
  **/
 enum clif_colors {
+	COLOR_DEFAULT,
 	COLOR_RED,
 	COLOR_WHITE,
 	COLOR_YELLOW,

+ 102 - 0
src/map/itemdb.c

@@ -929,6 +929,7 @@ static int itemdb_combo_split_atoi (char *str, int *val) {
 
 	return i;
 }
+
 /**
  * <combo{:combo{:combo:{..}}}>,<{ script }>
  **/
@@ -1056,7 +1057,101 @@ static void itemdb_read_combos(const char* basedir, bool silent) {
 	return;
 }
 
+/**
+ * Process Roulette items
+ */
+bool itemdb_parse_roulette_db(void)
+{
+	int i, j;
+	uint32 count = 0;
+
+	// retrieve all rows from the item database
+	if (SQL_ERROR == Sql_Query(mmysql_handle, "SELECT * FROM `%s`", db_roulette_table)) {
+		Sql_ShowDebug(mmysql_handle);
+		return false;
+	}
+
+	for (i = 0; i < MAX_ROULETTE_LEVEL; i++) {
+		rd.items[i] = 0;
+	}
+
+	// process rows one by one
+	while (SQL_SUCCESS == Sql_NextRow(mmysql_handle)) {
+		char* str[4];
+		char* dummy = "";
+		int i;
+
+		for (i = 0; i < MAX_ROULETTE_LEVEL; i++) {
+			struct item_data * data = NULL;
+
+			Sql_GetData(mmysql_handle, i, &str[i], NULL);
+
+			if (str[i] == NULL)
+				str[i] = dummy; // get rid of NULL columns
+
+			if (!(data = itemdb_exists(atoi(str[1])))) {
+				ShowWarning("itemdb_parse_roulette_db: Unknown item_id '%hu' in level '%d'\n", atoi(str[1]), atoi(str[0]));
+				continue;
+			}
+			if (atoi(str[2]) < 1) {
+				ShowWarning("itemdb_parse_roulette_db: Unsupported amount '%hu' for item_id '%hu' in level '%d'\n", atoi(str[2]), atoi(str[1]), atoi(str[0]));
+				continue;
+			}
+			if (atoi(str[3]) < 0 || atoi(str[3]) > 1) {
+				ShowWarning("itemdb_parse_roueltte_db: Unsupported flag '%d' for item_id '%hu' in level '%d'\n", atoi(str[3]), atoi(str[1]), atoi(str[0]));
+				continue;
+			}
+
+			j = rd.items[i];
+			RECREATE(rd.nameid[i], unsigned short, ++rd.items[i]);
+			RECREATE(rd.qty[i], unsigned short, rd.items[i]);
+			RECREATE(rd.flag[i], int, rd.items[i]);
+
+			rd.nameid[i][j] = data->nameid;
+			rd.qty[i][j] = atoi(str[2]);
+			rd.flag[i][j] = atoi(str[3]);
 
+			++count;
+		}
+	}
+
+	// free the query result
+	Sql_FreeResult(mmysql_handle);
+
+	for (i = 0; i < MAX_ROULETTE_LEVEL; i++) {
+		int limit = MAX_ROULETTE_COLUMNS - i;
+
+		if (rd.items[i] == limit)
+			continue;
+
+		if (rd.items[i] > limit) {
+			ShowWarning("itemdb_parse_roulette_db: level %d has %d items, only %d supported, capping...\n", i + 1, rd.items[i], limit);
+			rd.items[i] = limit;
+			continue;
+		}
+
+		/** this scenario = rd.items[i] < limit **/
+		ShowWarning("itemdb_parse_roulette_db: Level %d has %d items, %d are required. filling with apples\n", i + 1, rd.items[i], limit);
+
+		rd.items[i] = limit;
+		RECREATE(rd.nameid[i], unsigned short, rd.items[i]);
+		RECREATE(rd.qty[i], unsigned short, rd.items[i]);
+		RECREATE(rd.flag[i], int, rd.items[i]);
+
+		for (j = 0; j < MAX_ROULETTE_COLUMNS - i; j++) {
+			if (rd.qty[i][j])
+				continue;
+
+			rd.nameid[i][j] = ITEMID_APPLE;
+			rd.qty[i][j] = 1;
+			rd.flag[i][j] = 1;
+		}
+	}
+
+	ShowStatus("Done reading '"CL_WHITE"%lu"CL_RESET"' entries in '"CL_WHITE"%s"CL_RESET"'.\n", count, db_roulette_table);
+
+	return true;
+}
 
 /*======================================
  * Applies gender restrictions according to settings. [Skotlex]
@@ -1074,6 +1169,7 @@ static char itemdb_gendercheck(struct item_data *id)
 
 	return (battle_config.ignore_items_gender) ? 2 : id->sex;
 }
+
 /**
  * [RRInd]
  * For backwards compatibility, in Renewal mode, MATK from weapons comes from the atk slot
@@ -1579,6 +1675,9 @@ void itemdb_reload(void) {
 	itemdb_read();
 	cashshop_reloaddb();
 
+	if (!itemdb_parse_roulette_db())
+		battle_config.feature_roulette = 0;
+
 	//Epoque's awesome @reloaditemdb fix - thanks! [Ind]
 	//- Fixes the need of a @reloadmobdb after a @reloaditemdb to re-link monster drop data
 	for( i = 0; i < MAX_MOB_DB; i++ ) {
@@ -1649,4 +1748,7 @@ void do_init_itemdb(void) {
 	itemdb_group = uidb_alloc(DB_OPT_BASE);
 	itemdb_create_dummy();
 	itemdb_read();
+
+	if (!itemdb_parse_roulette_db())
+		battle_config.feature_roulette = 0;
 }

+ 14 - 0
src/map/itemdb.h

@@ -25,6 +25,9 @@
 
 #define MAX_ITEMGROUP_RANDGROUP 4	///Max group for random item (increase this when needed). TODO: Remove this limit and use dynamic size if needed
 
+#define MAX_ROULETTE_LEVEL 7 /** client-defined value **/
+#define MAX_ROULETTE_COLUMNS 9 /** client-defined value **/
+
 #define CARD0_FORGE 0x00FF
 #define CARD0_CREATE 0x00FE
 #define CARD0_PET 0x0100
@@ -39,6 +42,7 @@ enum item_itemid
 	ITEMID_YELLOW_POTION				= 503,
 	ITEMID_WHITE_POTION					= 504,
 	ITEMID_BLUE_POTION					= 505,
+	ITEMID_APPLE						= 512,
 	ITEMID_HOLY_WATER					= 523,
 	ITEMID_PUMPKIN						= 535,
 	ITEMID_RED_SLIM_POTION				= 545,
@@ -371,6 +375,14 @@ struct s_item_group_db
 	struct s_item_group_random random[MAX_ITEMGROUP_RANDGROUP]; //! TODO: Move this fixed array to dynamic size if needed.
 };
 
+/// Struct of Roulette db
+struct {
+	unsigned short *nameid[MAX_ROULETTE_LEVEL], /// Item ID
+		           *qty[MAX_ROULETTE_LEVEL]; /// Amount of Item ID
+	int *flag[MAX_ROULETTE_LEVEL]; /// Whether the item is for loss or win
+	int items[MAX_ROULETTE_LEVEL]; /// Number of items in the list for each
+} rd;
+
 ///Main item data struct
 struct item_data
 {
@@ -513,6 +525,8 @@ struct s_item_group_db *itemdb_group_exists(unsigned short group_id);
 char itemdb_pc_get_itemgroup(uint16 group_id, struct map_session_data *sd);
 uint16 itemdb_get_randgroupitem_count(uint16 group_id, uint8 sub_group, unsigned short nameid);
 
+bool itemdb_parse_roulette_db(void);
+
 void itemdb_reload(void);
 
 void do_final_itemdb(void);

+ 2 - 1
src/map/log.c

@@ -56,7 +56,7 @@ static char log_picktype2char(e_log_pick_type type)
 	{
 		case LOG_TYPE_TRADE:            return 'T';  // (T)rade
 		case LOG_TYPE_VENDING:          return 'V';  // (V)ending
-		case LOG_TYPE_PICKDROP_PLAYER:  return 'P';  // (P)player
+		case LOG_TYPE_PICKDROP_PLAYER:  return 'P';  // (P)layer
 		case LOG_TYPE_PICKDROP_MONSTER: return 'M';  // (M)onster
 		case LOG_TYPE_NPC:              return 'S';  // NPC (S)hop
 		case LOG_TYPE_SCRIPT:           return 'N';  // (N)PC Script
@@ -75,6 +75,7 @@ static char log_picktype2char(e_log_pick_type type)
 		case LOG_TYPE_OTHER:			return 'X';  // Other
 		case LOG_TYPE_CASH:				return '$';  // Cash
 		case LOG_TYPE_BOUND_REMOVAL:	return 'F';  // Removed bound items when guild/party is broken
+		case LOG_TYPE_ROULETTE:			return 'Y';  // Roulette Lotter(Y)
 	}
 
 	// should not get here, fallback

+ 22 - 21
src/map/log.h

@@ -24,30 +24,31 @@ typedef enum e_log_chat_type
 typedef enum e_log_pick_type
 {
 	LOG_TYPE_NONE             = 0,
-	LOG_TYPE_TRADE            = 0x00001,
-	LOG_TYPE_VENDING          = 0x00002,
-	LOG_TYPE_PICKDROP_PLAYER  = 0x00004,
-	LOG_TYPE_PICKDROP_MONSTER = 0x00008,
-	LOG_TYPE_NPC              = 0x00010,
-	LOG_TYPE_SCRIPT           = 0x00020,
-	LOG_TYPE_STEAL            = 0x00040,
-	LOG_TYPE_CONSUME          = 0x00080,
-	LOG_TYPE_PRODUCE          = 0x00100,
-	LOG_TYPE_MVP              = 0x00200,
-	LOG_TYPE_COMMAND          = 0x00400,
-	LOG_TYPE_STORAGE          = 0x00800,
-	LOG_TYPE_GSTORAGE         = 0x01000,
-	LOG_TYPE_MAIL             = 0x02000,
-	LOG_TYPE_AUCTION          = 0x04000,
-	LOG_TYPE_BUYING_STORE     = 0x08000,
-	LOG_TYPE_OTHER            = 0x10000,
-	LOG_TYPE_CASH             = 0x20000,
-	LOG_TYPE_BANK             = 0x40000,
-	LOG_TYPE_BOUND_REMOVAL    = 0x80000,
+	LOG_TYPE_TRADE            = 0x000001,
+	LOG_TYPE_VENDING          = 0x000002,
+	LOG_TYPE_PICKDROP_PLAYER  = 0x000004,
+	LOG_TYPE_PICKDROP_MONSTER = 0x000008,
+	LOG_TYPE_NPC              = 0x000010,
+	LOG_TYPE_SCRIPT           = 0x000020,
+	LOG_TYPE_STEAL            = 0x000040,
+	LOG_TYPE_CONSUME          = 0x000080,
+	LOG_TYPE_PRODUCE          = 0x000100,
+	LOG_TYPE_MVP              = 0x000200,
+	LOG_TYPE_COMMAND          = 0x000400,
+	LOG_TYPE_STORAGE          = 0x000800,
+	LOG_TYPE_GSTORAGE         = 0x001000,
+	LOG_TYPE_MAIL             = 0x002000,
+	LOG_TYPE_AUCTION          = 0x004000,
+	LOG_TYPE_BUYING_STORE     = 0x008000,
+	LOG_TYPE_OTHER            = 0x010000,
+	LOG_TYPE_CASH             = 0x020000,
+	LOG_TYPE_BANK             = 0x040000,
+	LOG_TYPE_BOUND_REMOVAL    = 0x080000,
+	LOG_TYPE_ROULETTE         = 0x100000,
 	// combinations
 	LOG_TYPE_LOOT             = LOG_TYPE_PICKDROP_MONSTER|LOG_TYPE_CONSUME,
 	// all
-	LOG_TYPE_ALL              = 0xFFFFF,
+	LOG_TYPE_ALL              = 0xFFFFFF,
 } e_log_pick_type;
 
 typedef enum e_log_cash_type

+ 4 - 1
src/map/map.c

@@ -70,6 +70,7 @@ char mob_skill_db2_db[32] = "mob_skill_db2";
 char vendings_db[32] = "vendings";
 char vending_items_db[32] = "vending_items";
 char market_table[32] = "market";
+char db_roulette_table[32] = "db_roulette";
 
 // log database
 char log_db_ip[32] = "127.0.0.1";
@@ -3759,7 +3760,9 @@ int inter_config_read(char *cfgName)
 		else if( strcmpi( w1, "vending_db" ) == 0 )
 			strcpy( vendings_db, w2 );
 		else if( strcmpi( w1, "vending_items_db" ) == 0 )
-			strcpy( vending_items_db, w2 );
+			strcpy(vending_items_db, w2);
+		else if( strcmpi(w1, "db_roulette_table") == 0)
+			strcpy(db_roulette_table, w2);
 		else if (strcmpi(w1, "market_table") == 0)
 			strcpy(market_table, w2);
 		else

+ 4 - 0
src/map/map.h

@@ -459,6 +459,9 @@ enum _sp {
 	SP_CHARRENAME=125,
 	SP_CHARFONT=126,
 	SP_BANK_VAULT = 127,
+	SP_ROULETTE_BRONZE = 128,
+	SP_ROULETTE_SILVER = 129,
+	SP_ROULETTE_GOLD = 130,
 
 	// Mercenaries
 	SP_MERCFLEE=165, SP_MERCKILLS=189, SP_MERCFAITH=190,
@@ -1006,6 +1009,7 @@ extern char mob_skill_db2_db[32];
 extern char vendings_db[32];
 extern char vending_items_db[32];
 extern char market_table[32];
+extern char db_roulette_table[32];
 
 void do_shutdown(void);
 

+ 22 - 1
src/map/pc.c

@@ -1344,6 +1344,12 @@ void pc_reg_received(struct map_session_data *sd)
 	if (battle_config.feature_banking)
 		sd->bank_vault = pc_readreg2(sd, BANK_VAULT_VAR);
 
+	if (battle_config.feature_roulette) {
+		sd->roulette_point.bronze = pc_readreg2(sd, ROULETTE_BRONZE_VAR);
+		sd->roulette_point.silver = pc_readreg2(sd, ROULETTE_SILVER_VAR);
+		sd->roulette_point.gold = pc_readreg2(sd, ROULETTE_GOLD_VAR);
+	}
+
 	//SG map and mob read [Komurka]
 	for(i=0;i<MAX_PC_FEELHATE;i++) { //for now - someone need to make reading from txt/sql
 		uint16 j;
@@ -7602,7 +7608,10 @@ int pc_readparam(struct map_session_data* sd,int type)
 		case SP_CHARMOVE:		 val = sd->status.character_moves; break;
 		case SP_CHARRENAME:		 val = sd->status.rename; break;
 		case SP_CHARFONT:		 val = sd->status.font; break;
-		case SP_BANK_VAULT:			 val = sd->bank_vault; break;
+		case SP_BANK_VAULT:      val = sd->bank_vault; break;
+		case SP_ROULETTE_BRONZE: val = sd->roulette_point.bronze; break;
+		case SP_ROULETTE_SILVER: val = sd->roulette_point.silver; break;
+		case SP_ROULETTE_GOLD:   val = sd->roulette_point.gold; break;
 		case SP_CRITICAL:        val = sd->battle_status.cri/10; break;
 		case SP_ASPD:            val = (2000-sd->battle_status.amotion)/10; break;
 		case SP_BASE_ATK:	     val = sd->battle_status.batk; break;
@@ -7861,6 +7870,18 @@ bool pc_setparam(struct map_session_data *sd,int type,int val)
 		sd->bank_vault = cap_value(val, 0, MAX_BANK_ZENY);
 		pc_setreg2(sd, BANK_VAULT_VAR, sd->bank_vault);
 		return true;
+	case SP_ROULETTE_BRONZE:
+		sd->roulette_point.bronze = val;
+		pc_setreg2(sd, ROULETTE_BRONZE_VAR, sd->roulette_point.bronze);
+		return true;
+	case SP_ROULETTE_SILVER:
+		sd->roulette_point.silver = val;
+		pc_setreg2(sd, ROULETTE_SILVER_VAR, sd->roulette_point.silver);
+		return true;
+	case SP_ROULETTE_GOLD:
+		sd->roulette_point.gold = val;
+		pc_setreg2(sd, ROULETTE_GOLD_VAR, sd->roulette_point.gold);
+		return true;
 	default:
 		ShowError("pc_setparam: Attempted to set unknown parameter '%d'.\n", type);
 		return false;

+ 14 - 0
src/map/pc.h

@@ -31,6 +31,9 @@
 #define MAX_SPIRITCHARM 10 /// Max spirit charms
 
 #define BANK_VAULT_VAR "#BANKVAULT"
+#define ROULETTE_BRONZE_VAR "RouletteBronze"
+#define ROULETTE_SILVER_VAR "RouletteSilver"
+#define ROULETTE_GOLD_VAR "RouletteGold"
 
 //Update this max as necessary. 55 is the value needed for Super Baby currently
 //Raised to 84 since Expanded Super Novice needs it.
@@ -640,6 +643,17 @@ struct map_session_data {
 #ifdef PACKET_OBFUSCATION
 	unsigned int cryptKey; ///< Packet obfuscation key to be used for the next received packet
 #endif
+
+	struct {
+		int bronze, silver, gold; ///< Roulette Coin
+	} roulette_point;
+
+	struct {
+		short stage;
+		short prizeIdx;
+		short prizeStage;
+		bool claimPrize;
+	} roulette;
 };
 
 struct eri *pc_sc_display_ers; /// Player's SC display table

+ 36 - 5
src/map/script.c

@@ -10981,9 +10981,9 @@ BUILDIN_FUNC(changebase)
 }
 
 /**
- * Change sec and unequip all item and request for a changesex to char-serv
+ * Change account sex and unequip all item and request for a changesex to char-serv
  * changesex({<char_id>});
- **/
+ */
 BUILDIN_FUNC(changesex)
 {
 	int i;
@@ -10994,12 +10994,42 @@ BUILDIN_FUNC(changesex)
 
 	pc_resetskill(sd,4);
 	// to avoid any problem with equipment and invalid sex, equipment is unequiped.
-	for( i=0; i<EQI_MAX; i++ )
-		if( sd->equip_index[i] >= 0 ) pc_unequipitem(sd, sd->equip_index[i], 3);
-	chrif_changesex(sd);
+	for(i = 0; i < EQI_MAX; i++) {
+		if (sd->equip_index[i] >= 0)
+			pc_unequipitem(sd, sd->equip_index[i], 3);
+	}
+
+	chrif_changesex(sd, true);
 	return SCRIPT_CMD_SUCCESS;
 }
 
+/**
+ * Change character's sex and unequip all item and request for a changesex to char-serv
+ * changecharsex({<char_id>});
+ */
+BUILDIN_FUNC(changecharsex)
+{
+#if PACKETVER >= 20141016
+	int i;
+	TBL_PC *sd = NULL;
+
+	if (!script_charid2sd(2,sd))
+		return SCRIPT_CMD_FAILURE;
+
+	pc_resetskill(sd,4);
+	// to avoid any problem with equipment and invalid sex, equipment is unequiped.
+	for (i = 0; i < EQI_MAX; i++) {
+		if (sd->equip_index[i] >= 0)
+			pc_unequipitem(sd, sd->equip_index[i], 3);
+	}
+
+	chrif_changesex(sd, false);
+	return SCRIPT_CMD_SUCCESS;
+#else
+	return SCRIPT_CMD_FAILURE;
+#endif
+}
+
 /*==========================================
  * Works like 'announce' but outputs in the common chat window
  *------------------------------------------*/
@@ -20486,6 +20516,7 @@ struct script_function buildin_func[] = {
 	BUILDIN_DEF(skillpointcount,"?"),
 	BUILDIN_DEF(changebase,"i?"),
 	BUILDIN_DEF(changesex,"?"),
+	BUILDIN_DEF(changecharsex,"?"),
 	BUILDIN_DEF(waitingroom,"si?????"),
 	BUILDIN_DEF(delwaitingroom,"?"),
 	BUILDIN_DEF2(waitingroomkickall,"kickwaitingroomall","?"),