Просмотр исходного кода

Initial Release of Attendance Feature (#3297)

Thanks to @secretdataz and @aleos89 for their help.
Thanks to @Haikenz and @admkakaroto for testing.
Thanks to @Daegaladh for his ideas.
Lemongrass3110 6 лет назад
Родитель
Сommit
a5588dd9ab

+ 4 - 0
conf/battle/feature.conf

@@ -70,3 +70,7 @@ feature.achievement: on
 // Homunculues Autofeeding (Note 1)
 // Requires: 2017-09-20bRagexeRE or later
 feature.homunculus_autofeed: off
+
+// Attendance System (Note 1)
+// Requires: 2018-03-07bRagexeRE or later
+feature.attendance: off

+ 2 - 0
conf/groups.conf

@@ -94,6 +94,7 @@ groups: (
 		can_trade: true
 		can_party: true
 		command_enable: true
+		attendance: true
 	}
 },
 {
@@ -135,6 +136,7 @@ groups: (
 		langtype: true
 	}
 	permissions: {
+		attendance: false
 	}
 },
 {

+ 3 - 1
conf/map_athena.conf

@@ -105,11 +105,13 @@ minsave_time: 100
 // 32: After successfully submitting an item for auction
 // 64: After successfully get/delete/complete a quest
 // 128: After every bank transaction (deposit/withdraw)
+// 256: After every attendance reward
+// 4095: Always
 // NOTE: These settings decrease the chance of dupes/lost items when there's a
 // server crash at the expense of increasing the map/char server lag. If your 
 // server rarely crashes, but experiences interserver lag, you may want to set
 // these off.
-save_settings: 255
+save_settings: 4095
 
 // Message of the day file, when a character logs on, this message is displayed.
 motd_txt: conf/motd.txt

+ 10 - 1
conf/msg_conf/map_msg.conf

@@ -843,7 +843,16 @@
 786: The guild does not have a guild storage.
 787: You do not have permission to use the guild storage.
 
-//788-899 free
+// Attendance
+// Mail sender: Officer
+788: <MSG>3455</MSG>
+// Mail title: %dday attendance has been paid.
+789: <MSG>3456,%d</MSG>
+// Mail body: %dday attendance has been paid.
+790: <MSG>3456,%d</MSG>
+791: You are not allowed to use the attendance system.
+
+//792-899 free
 
 //------------------------------------
 // More atcommands message

+ 3 - 0
db/import-tmpl/attendance.yml

@@ -0,0 +1,3 @@
+Header:
+  Type: ATTENDANCE_CONF
+  Version: 1

+ 3 - 0
db/pre-re/attendance.yml

@@ -0,0 +1,3 @@
+Header:
+  Type: ATTENDANCE_CONF
+  Version: 1

+ 61 - 0
db/re/attendance.yml

@@ -0,0 +1,61 @@
+Header:
+  Type: ATTENDANCE_CONF
+  Version: 1
+  
+Attendance:
+  - Start: 20180502
+    End: 20180529
+    Rewards:
+      - Day: 1
+        ItemId: 22979
+      - Day: 2
+        ItemId: 6316
+      - Day: 3
+        ItemId: 12265
+        Amount: 5
+      - Day: 4
+        ItemId: 23047
+        Amount: 5
+      - Day: 5
+        ItemId: 23038
+      - Day: 6
+        ItemId: 23043
+      - Day: 7
+        ItemId: 23340
+        Amount: 3
+      - Day: 8
+        ItemId: 12516
+        Amount: 5
+      - Day: 9
+        ItemId: 23307
+        Amount: 5
+      - Day: 10
+        ItemId: 12610
+      - Day: 11
+        ItemId: 14533
+        Amount: 2
+      - Day: 12
+        ItemId: 23012
+        Amount: 3
+      - Day: 13
+        ItemId: 23048
+        Amount: 5
+      - Day: 14
+        ItemId: 12264
+        Amount: 5
+      - Day: 15
+        ItemId: 23046
+        Amount: 5
+      - Day: 16
+        ItemId: 12515
+        Amount: 5
+      - Day: 17
+        ItemId: 12522
+        Amount: 5
+      - Day: 18
+        ItemId: 12523
+        Amount: 5
+      - Day: 19
+        ItemId: 6234
+      - Day: 20
+        ItemId: 22845

+ 13 - 1
db/re/item_db.txt

@@ -6827,6 +6827,7 @@
 12607,Lolli_Pop_Box,Delicious Lollipop Box,11,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
 12608,Splendid_Box2,Splendid Box2,11,20,,100,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
 12609,Old_Ore_Box,Old Ore Box,2,20,,100,,,,,0xFFFFFFFF,63,2,,,,,,{ getgroupitem(IG_Old_Ore_Box); },{},{}
+12610,Mysterious_Egg,Mysterious Egg,2,,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
 12612,Old_Coin_Pocket,Old Coin Bag,2,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{ getgroupitem(IG_Old_Coin_Pocket); },{},{}
 12613,High_Coin_Pocket,Improved Coin Bag,2,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{ getgroupitem(IG_High_Coin_Pocket); },{},{}
 12614,Mid_Coin_Pocket,Intermediate Coin Bag,2,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{ getgroupitem(IG_Mid_Coin_Pocket); },{},{}
@@ -9277,7 +9278,7 @@
 17181,Jan_Groove_Box,Jan Groove Box,2,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
 17184,3rd_Test_Pass_Box,3rd Test Pass Box,18,0,,0,,,,,0xFFFFFFFF,63,2,,,,,,{ getitem 6583,1; },{},{}
 17203,Free_Pass_Box,Free Pass Box,2,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
-17204,Mysterious_Egg,Shining Egg,18,10,,10,,,,0,0xFFFFFFFF,63,2,,,1,,,{ getgroupitem(IG_Mysterious_Egg); },{},{}
+17204,Shining_Egg,Shining Egg,18,10,,10,,,,0,0xFFFFFFFF,63,2,,,1,,,{ getgroupitem(IG_Shining_Egg); },{},{}
 17207,Idn_Heart_Scroll,Idn Heart Scroll,2,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{ getgroupitem(IG_Idn_Heart_Scroll); },{},{}
 17209,Tw_Rainbow_Scroll,Tw Rainbow Scroll,2,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{ getgroupitem(IG_Tw_Rainbow_Scroll); },{},{}
 17210,Tw_Red_Scroll,Tw Red Scroll,2,20,,10,,,,,0xFFFFFFFF,63,2,,,,,,{ getgroupitem(IG_Tw_Red_Scroll); },{},{}
@@ -11155,6 +11156,7 @@
 22782,PC_Bang_Wooden_Box,PC Bang Wooden Box,2,10,,200,,,,0,0xFFFFFFFF,63,2,,,1,,,{ getitem 547,30; /*No Info*/},{},{}
 22783,PC_Bang_Golden_Box,PC Bang Golden Box,2,10,,200,,,,0,0xFFFFFFFF,63,2,,,1,,,{ getitem 547,1; getitem 985,10; /*No Info*/},{},{}
 22784,PC_Bang_Platinum_Box,PC Bang Platinum Box,2,10,,200,,,,0,0xFFFFFFFF,63,2,,,1,,,{ getitem 547,1; getitem 12017,10; getitem 678,12; /*No Info*/},{},{}
+22979,C_Battle_Gum_2,[Sale] Battle Manual and Bubble Gum,2,,,0,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
 22802,Safe_to_6_Equipment_Certificate,Safe to 6 Equipment Certificate,3,10,,10,,,,,,,,,,,,,{},{},{}
 22808,Special_Gift_Box,Special Gift Box,2,10,,100,,,,,,,,,,,,,{},{},{}
 22812,Sealed_Dracula_Scroll,Sealed Dracula Scroll,2,10,,10,,,,0,0xFFFFFFFF,63,2,,,1,,,{ getitem callfunc("F_Rand",6228,6232,22813,19937,17314, 6635),1; },{},{}
@@ -11213,6 +11215,13 @@
 22984,Kahluna_Milk,Kahluna Milk,0,6,,10,,,,,0xFFFFFFFF,63,2,,,,,,{ sc_start SC_DORAM_BUF_01, 180000, 0; },{},{}
 22985,Basil,Basil,0,10,,10,,,,,0xFFFFFFFF,63,2,,,,,,{ sc_start SC_DORAM_BUF_02, 180000, 0; },{},{}
 //
+23012,S_Small_Mana_Potion,[Sale] Small Mana Potion,2,,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
+23038,S_Slim_White_Box,[Sale] Slim White Potion Box,2,,,0,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
+23043,S_Seed_Of_Yggdrasil_Box,[Sale] Yggdrasil Seed Box,2,,,0,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
+23046,S_Mystic_Powder,[Sale] Mystic Powder,2,,,0,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
+23047,S_Blessing_Tyr,[Sale] Blessing of Tyr,2,,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
+23048,S_Resilience_Potion,[Sale] Resilience Enhancement Potion,2,,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
+//
 23123,Bullet_Case_Flare,Flare Bullet Cartridge,2,10,,250,,,,,0xFFFFFFFF,63,2,,,,,,{ getitem 13228,500; },{},{}
 23124,Bullet_Case_Lighting,Lightning Bullet Cartridge,2,10,,250,,,,,0xFFFFFFFF,63,2,,,,,,{ getitem 13229,500; },{},{}
 23125,Bullet_Case_Ice,Ice Bullet Cartridge,2,10,,250,,,,,0xFFFFFFFF,63,2,,,,,,{ getitem 13230,500; },{},{}
@@ -11227,6 +11236,9 @@
 23196,Agust_Lucky_Scroll,Shining Blue Lucky Egg,18,10,,10,,,,0,0xFFFFFFFF,63,2,,,1,,,{ getgroupitem(IG_Agust_Lucky_Scroll); },{},{}
 //
 23277,Mado_Box,Emergency Magic Gear,2,10000,,3000,,,,,0x00000400,56,2,,,100,,,{ setmadogear 1; },{},{}
+//
+23307,S_Shining_Def_Scroll,[Sale] Shining Defense Scroll,2,,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
+23340,S_Megaphone,[Sale] Megaphone,2,,,10,,,,,0xFFFFFFFF,63,2,,,,,,{},{},{}
 //===================================================================
 // Shadow Equipments
 //===================================================================

+ 17 - 17
db/re/item_package.txt

@@ -4473,23 +4473,23 @@ IG_Something_Candy_Holder,22067,1,1 // 1x Witch Shoes
 IG_Something_Candy_Holder,22669,5,1 // 1x October Spooky Trade Box
 IG_Something_Candy_Holder,22670,1,1 // 1x DARK INVITATION
 
-// Mysterious_Egg
-IG_Mysterious_Egg,12259,200,1,1,0,0,0	// 1x Miracle_Medicine
-IG_Mysterious_Egg,5374,1,1,1,0,0,0	// 1x L_Magestic_Goat
-IG_Mysterious_Egg,5254,99,1,1,0,0,0	// 1x Deviling_Hat
-IG_Mysterious_Egg,12246,99,1,1,0,0,0	// 1x Magic_Card_Album
-IG_Mysterious_Egg,4302,1,1,1,0,0,0	// 1x Tao_Gunka_Card
-IG_Mysterious_Egg,5474,200,1,1,0,0,0	// 1x Notice_Board
-IG_Mysterious_Egg,2554,1,1,1,0,0,0	// 1x Piece_Of_Angent_Skin
-IG_Mysterious_Egg,17001,1099,1,1,0,0,0	// 1x Wander_Man_Box10
-IG_Mysterious_Egg,12903,1000,1,1,0,0,0	// 1x Str_Dish_Box
-IG_Mysterious_Egg,12922,1100,1,1,0,0,0	// 1x Token_Of_Siegfried_Box
-IG_Mysterious_Egg,16755,1200,1,1,0,0,0	// 1x Unbreak_Def_Box
-IG_Mysterious_Egg,12909,800,1,1,0,0,0	// 1x Kafra_Card_Box
-IG_Mysterious_Egg,14232,800,1,1,0,0,0	// 1x Yggdrasilberry_Box_
-IG_Mysterious_Egg,12361,1000,2,1,0,0,0	// 2x Delicious_Shaved_Ice
-IG_Mysterious_Egg,12910,1100,1,1,0,0,0	// 1x Giant_Fly_Wing_Box
-IG_Mysterious_Egg,16753,1300,1,1,0,0,0	// 1x Unbreak_Weap_Box
+// Shining Egg
+IG_Shining_Egg,12259,200,1,1,0,0,0	// 1x Miracle_Medicine
+IG_Shining_Egg,5374,1,1,1,0,0,0	// 1x L_Magestic_Goat
+IG_Shining_Egg,5254,99,1,1,0,0,0	// 1x Deviling_Hat
+IG_Shining_Egg,12246,99,1,1,0,0,0	// 1x Magic_Card_Album
+IG_Shining_Egg,4302,1,1,1,0,0,0	// 1x Tao_Gunka_Card
+IG_Shining_Egg,5474,200,1,1,0,0,0	// 1x Notice_Board
+IG_Shining_Egg,2554,1,1,1,0,0,0	// 1x Piece_Of_Angent_Skin
+IG_Shining_Egg,17001,1099,1,1,0,0,0	// 1x Wander_Man_Box10
+IG_Shining_Egg,12903,1000,1,1,0,0,0	// 1x Str_Dish_Box
+IG_Shining_Egg,12922,1100,1,1,0,0,0	// 1x Token_Of_Siegfried_Box
+IG_Shining_Egg,16755,1200,1,1,0,0,0	// 1x Unbreak_Def_Box
+IG_Shining_Egg,12909,800,1,1,0,0,0	// 1x Kafra_Card_Box
+IG_Shining_Egg,14232,800,1,1,0,0,0	// 1x Yggdrasilberry_Box_
+IG_Shining_Egg,12361,1000,2,1,0,0,0	// 2x Delicious_Shaved_Ice
+IG_Shining_Egg,12910,1100,1,1,0,0,0	// 1x Giant_Fly_Wing_Box
+IG_Shining_Egg,16753,1300,1,1,0,0,0	// 1x Unbreak_Weap_Box
 
 // Agust_Lucky_Scroll
 IG_Agust_Lucky_Scroll,4365,1,1,1,1,0,0	// 1x B_Katrinn_Card

+ 10 - 1
db/re/item_trade.txt

@@ -1440,7 +1440,7 @@
 12596,475,100	// Magic_Candy
 12600,507,100	// Treasure_Box_Scroll
 12607,507,100	// Lolli_Pop_Box
-//12610,475,100	//
+12610,475,100	// Mysterious_Egg
 12622,507,100	// Boarding_Halter
 12625,475,100	// Sapa_Feat_Cert_Pack
 12633,475,100	// Malang_Cat_Can
@@ -3869,6 +3869,15 @@
 //22950,475,100	//
 //22951,475,100	//
 //22952,475,100	//
+22979,475,100	// C_Battle_Gum_2
+23012,475,100	// S_Small_Mana_Potion
+23038,475,100	// S_Slim_White_Box
+23043,475,100	// S_Seed_Of_Yggdrasil_Box
+23046,475,100	// S_Mystic_Powder
+23047,475,100	// S_Blessing_Tyr
+23048,475,100	// S_Resilience_Potion
+23307,475,100	// S_Shining_Def_Scroll
+23340,475,100	// S_Megaphone
 23177,475,100	// Kafra_Card_
 23196,475,100	// Shining_Blue_Lucky_Egg
 25043,499,100	// Thorny_Vine_Flute

+ 1 - 0
doc/script_commands.txt

@@ -3135,6 +3135,7 @@ DT_DAYOFMONTH - Day of the current month
 DT_MONTH - Month (constants for JANUARY to DECEMBER are available)
 DT_YEAR - Year
 DT_DAYOFYEAR - Day of the year
+DT_YYYYMMDD - current date in the form YYYYMMDD
 
 It will only return numbers. If another type is supplied -1 will be returned.
 

+ 6 - 0
src/common/utilities.hpp

@@ -8,6 +8,8 @@
 #include <string>
 #include <map>
 
+#include "cbasetypes.hpp"
+
 // Class used to perform time measurement
 class cScopeTimer {
 	struct sPimpl; //this is to avoid long compilation time
@@ -20,6 +22,10 @@ int levenshtein( const std::string &s1, const std::string &s2 );
 
 namespace rathena {
 	namespace util {
+		template <typename K, typename V> bool map_exists( std::map<K,V>& map, K key ){
+			return map.find( key ) != map.end();
+		}
+
 		template <typename K, typename V> V* map_find( std::map<K,V>& map, K key ){
 			auto it = map.find( key );
 

+ 8 - 0
src/map/battle.cpp

@@ -8562,6 +8562,7 @@ static const struct _battle_data {
 	{ "feature.homunculus_autofeed",        &battle_config.feature_homunculus_autofeed,     1,      0,      1,              },
 	{ "summoner_trait",                     &battle_config.summoner_trait,                  3,      0,      3,              },
 	{ "homunculus_autofeed_always",         &battle_config.homunculus_autofeed_always,      1,      0,      1,              },
+	{ "feature.attendance",                 &battle_config.feature_attendance,              1,      0,      1,              },
 
 #include "../custom/battle_config_init.inc"
 };
@@ -8699,6 +8700,13 @@ void battle_adjust_conf()
 	}
 #endif
 
+#if PACKETVER < 20180307
+	if( battle_config.feature_attendance ){
+		ShowWarning("conf/battle/feature.conf attendance system is enabled but it requires PACKETVER 2018-03-07 or newer, disabling...\n");
+		battle_config.feature_attendance = 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.hpp

@@ -643,6 +643,7 @@ struct Battle_Config
 	int feature_homunculus_autofeed;
 	int summoner_trait;
 	int homunculus_autofeed_always;
+	int feature_attendance;
 
 #include "../custom/battle_config_struct.inc"
 };

+ 65 - 0
src/map/clif.cpp

@@ -9930,6 +9930,21 @@ void clif_msg_skill(struct map_session_data* sd, uint16 skill_id, int msg_id)
 	WFIFOSET(fd, packet_len(0x7e6));
 }
 
+/// Displays msgstringtable.txt string in a color. (ZC_MSG_COLOR).
+/// 09cd <msg id>.W <color>.L
+void clif_msg_color( struct map_session_data *sd, uint16 msg_id, uint32 color ){
+	nullpo_retv(sd);
+
+	int fd = sd->fd;
+
+	WFIFOHEAD(fd, packet_len(0x9cd));
+	WFIFOW(fd, 0) = 0x9cd;
+	WFIFOW(fd, 2) = msg_id;
+	WFIFOL(fd, 4) = color;
+
+	WFIFOSET(fd, packet_len(0x9cd));
+}
+
 /// Validates one global/guild/party/whisper message packet and tries to recognize its components.
 /// Returns true if the packet was parsed successfully.
 /// Formats: false - <packet id>.w <packet len>.w (<name> : <message>).?B 00
@@ -20285,6 +20300,56 @@ void clif_parse_changedress( int fd, struct map_session_data* sd ){
 #endif
 }
 
+/// Opens an UI window of the given type and initializes it with the given data
+/// 0AE2 <type>.B <data>.L
+void clif_ui_open( struct map_session_data *sd, enum out_ui_type ui_type, int32 data ){
+	nullpo_retv(sd);
+
+	int fd = sd->fd;
+
+	WFIFOHEAD(fd,packet_len(0xae2));
+	WFIFOW(fd,0) = 0xae2;
+	WFIFOB(fd,2) = ui_type;
+	WFIFOL(fd,3) = data;
+	WFIFOSET(fd,packet_len(0xae2));
+}
+
+/// Request to open an UI window of the given type
+/// 0A68 <type>.B
+void clif_parse_open_ui( int fd, struct map_session_data* sd ){
+	switch( RFIFOB(fd,2) ){
+		case IN_UI_ATTENDANCE:
+			if( !pc_has_permission( sd, PC_PERM_ATTENDANCE ) ){
+				clif_messagecolor( &sd->bl, color_table[COLOR_RED], msg_txt( sd, 791 ), false, SELF ); // You are not allowed to use the attendance system.
+			}else if( pc_attendance_enabled() ){
+				clif_ui_open( sd, OUT_UI_ATTENDANCE, pc_attendance_counter( sd ) );
+			}else{
+				clif_msg_color( sd, MSG_ATTENDANCE_DISABLED, color_table[COLOR_RED] );
+			}
+			break;
+	}
+}
+
+/// Response for attedance request
+/// 0AF0 <unknown>.L <data>.L
+void clif_attendence_response( struct map_session_data *sd, int32 data ){
+	nullpo_retv(sd);
+
+	int fd = sd->fd;
+
+	WFIFOHEAD(fd,packet_len(0xAF0));
+	WFIFOW(fd,0) = 0xAF0;
+	WFIFOL(fd,2) = 0;
+	WFIFOL(fd,6) = data;
+	WFIFOSET(fd,packet_len(0xAF0));
+}
+
+/// Request from the client to retrieve today's attendance reward
+/// 0AEF
+void clif_parse_attendance_request( int fd, struct map_session_data* sd ){
+	pc_attendance_claim_reward(sd);
+}
+
 /*==========================================
  * Main client packet processing function
  *------------------------------------------*/

+ 13 - 0
src/map/clif.hpp

@@ -510,6 +510,7 @@ enum clif_messages : uint16_t {
 	SKILL_NEED_REVOLVER = 0x9fd,
 	SKILL_NEED_HOLY_BULLET = 0x9fe,
 	SKILL_NEED_GRENADE = 0xa01,
+	MSG_ATTENDANCE_DISABLED = 0xd92,
 };
 
 enum e_personalinfo : uint8_t {
@@ -1075,4 +1076,16 @@ void clif_achievement_list_all(struct map_session_data *sd);
 void clif_achievement_update(struct map_session_data *sd, struct achievement *ach, int count);
 void clif_achievement_reward_ack(int fd, unsigned char result, int ach_id);
 
+/// Attendance System
+enum in_ui_type : int8 {
+	IN_UI_ATTENDANCE = 5
+};
+
+enum out_ui_type : int8 {
+	OUT_UI_ATTENDANCE = 7
+};
+
+void clif_ui_open( struct map_session_data *sd, enum out_ui_type ui_type, int32 data );
+void clif_attendence_response( struct map_session_data *sd, int32 data );
+
 #endif /* _CLIF_HPP_ */

+ 3 - 2
src/map/clif_packetdb.hpp

@@ -2143,6 +2143,7 @@
 	parseable_packet(0x096E,-1,clif_parse_merge_item_req,2,4); // CZ_REQ_MERGE_ITEM
 	ack_packet(ZC_ACK_MERGE_ITEM,0x096F,7,2,4,6,7); // ZC_ACK_MERGE_ITEM
 	parseable_packet(0x0974,2,clif_parse_merge_item_cancel,0); // CZ_CANCEL_MERGE_ITEM
+	packet(0x9CD,8); // ZC_MSG_COLOR
 #endif
 
 // 2013-08-21bRagexe
@@ -2381,9 +2382,9 @@
 
 // 2018-03-07bRagexeRE
 #if PACKETVER >= 20180307
-	parseable_packet(0x0A68,3,clif_parse_dull,0);
+	parseable_packet(0x0A68,3,clif_parse_open_ui,2);
 	packet(0x0AE2,7);
-	parseable_packet(0x0AEF,2,clif_parse_dull,0);
+	parseable_packet(0x0AEF,2,clif_parse_attendance_request,0);
 	packet(0x0AF0,10);
 #endif
 

+ 2 - 0
src/map/date.cpp

@@ -123,6 +123,8 @@ int date_get( enum e_date_type type )
 			return date_get_year();
 		case DT_DAYOFYEAR:
 			return date_get_dayofyear();
+		case DT_YYYYMMDD:
+			return date_get( DT_YEAR ) * 10000 + date_get( DT_MONTH ) * 100 + date_get(DT_DAYOFMONTH);
 		default:
 			return -1;
 	}

+ 1 - 0
src/map/date.hpp

@@ -41,6 +41,7 @@ enum e_date_type{
 	DT_MONTH,
 	DT_YEAR,
 	DT_DAYOFYEAR,
+	DT_YYYYMMDD,
 	DT_MAX
 };
 

+ 1 - 1
src/map/itemdb.hpp

@@ -714,7 +714,7 @@ enum e_random_item_group {
 	IG_COSTAMA_EGG29,
 	IG_INK_BALL,
 	IG_SOMETHING_CANDY_HOLDER,
-	IG_MYSTERIOUS_EGG,
+	IG_SHINING_EGG,
 	IG_AGUST_LUCKY_SCROLL,
 	IG_ELEMENT,
 	IG_POISON,

+ 1 - 0
src/map/map-server.vcxproj

@@ -292,6 +292,7 @@
     <Copy SourceFiles="$(SolutionDir)conf\msg_conf\import-tmpl\map_msg_tha_conf.txt" DestinationFolder="$(SolutionDir)conf\msg_conf\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)conf\msg_conf\import\map_msg_tha_conf.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\abra_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\abra_db.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\achievement_db.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\achievement_db.yml')" />
+    <Copy SourceFiles="$(SolutionDir)db\import-tmpl\attendance.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\attendance.yml')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\attr_fix.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\attr_fix.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\castle_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\castle_db.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\const.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\const.txt')" />

+ 1 - 1
src/map/map.cpp

@@ -125,7 +125,7 @@ int map_port=0;
 
 int autosave_interval = DEFAULT_AUTOSAVE_INTERVAL;
 int minsave_interval = 100;
-unsigned char save_settings = CHARSAVE_ALL;
+int16 save_settings = CHARSAVE_ALL;
 bool agit_flag = false;
 bool agit2_flag = false;
 bool agit3_flag = false;

+ 12 - 11
src/map/map.hpp

@@ -753,7 +753,7 @@ extern int map_num;
 
 extern int autosave_interval;
 extern int minsave_interval;
-extern unsigned char save_settings;
+extern int16 save_settings;
 extern int night_flag; // 0=day, 1=night [Yor]
 extern int enable_spy; //Determines if @spy commands are active.
 
@@ -780,16 +780,17 @@ extern struct s_map_default map_default;
 
 /// Type of 'save_settings'
 enum save_settings_type {
-	CHARSAVE_NONE = 0,
-	CHARSAVE_TRADE   = 0x01, /// After trading
-	CHARSAVE_VENDING = 0x02, /// After vending (open/transaction)
-	CHARSAVE_STORAGE = 0x04, /// After closing storage/guild storage.
-	CHARSAVE_PET     = 0x08, /// After hatching/returning to egg a pet.
-	CHARSAVE_MAIL    = 0x10, /// After successfully sending a mail with attachment
-	CHARSAVE_AUCTION = 0x20, /// After successfully submitting an item for auction
-	CHARSAVE_QUEST   = 0x40, /// After successfully get/delete/complete a quest
-	CHARSAVE_BANK    = 0x80, /// After every bank transaction (deposit/withdraw)
-	CHARSAVE_ALL     = 0xFF,
+	CHARSAVE_NONE		= 0x000, /// Never
+	CHARSAVE_TRADE		= 0x001, /// After trading
+	CHARSAVE_VENDING	= 0x002, /// After vending (open/transaction)
+	CHARSAVE_STORAGE	= 0x004, /// After closing storage/guild storage.
+	CHARSAVE_PET		= 0x008, /// After hatching/returning to egg a pet.
+	CHARSAVE_MAIL		= 0x010, /// After successfully sending a mail with attachment
+	CHARSAVE_AUCTION	= 0x020, /// After successfully submitting an item for auction
+	CHARSAVE_QUEST		= 0x040, /// After successfully get/delete/complete a quest
+	CHARSAVE_BANK		= 0x080, /// After every bank transaction (deposit/withdraw)
+	CHARSAVE_ATTENDANCE	= 0x100, /// After every attendence reward
+	CHARSAVE_ALL		= 0xFFF, /// Always
 };
 
 // users

+ 283 - 0
src/map/pc.cpp

@@ -3,9 +3,14 @@
 
 #include "pc.hpp"
 
+#include <map>
+#include <vector>
+
 #include <math.h>
 #include <stdlib.h>
 
+#include <yaml-cpp/yaml.h>
+
 #include "../common/cbasetypes.hpp"
 #include "../common/core.hpp" // get_svn_revision()
 #include "../common/ers.hpp"  // ers_destroy
@@ -17,6 +22,7 @@
 #include "../common/socket.hpp" // session[]
 #include "../common/strlib.hpp" // safestrncpy()
 #include "../common/timer.hpp"
+#include "../common/utilities.hpp"
 #include "../common/utils.hpp"
 
 #include "achievement.hpp"
@@ -53,7 +59,10 @@
 #include "unit.hpp" // unit_stop_attack(), unit_stop_walking()
 #include "vending.hpp" // struct s_vending
 
+using namespace rathena;
+
 int pc_split_atoui(char* str, unsigned int* val, char sep, int max);
+static inline bool pc_attendance_rewarded_today( struct map_session_data* sd );
 
 #define PVP_CALCRANK_INTERVAL 1000	// PVP calculation interval
 #define MAX_LEVEL_BASE_EXP 99999999 ///< Max Base EXP for player on Max Base Level
@@ -82,6 +91,19 @@ struct fame_list taekwon_fame_list[MAX_FAME_LIST];
 
 struct s_job_info job_info[CLASS_COUNT];
 
+struct s_attendance_reward{
+	uint16 item_id;
+	uint16 amount;
+};
+
+struct s_attendance_period{
+	uint32 start;
+	uint32 end;
+	std::map<int,struct s_attendance_reward> rewards;
+};
+
+std::vector<struct s_attendance_period> attendance_periods;
+
 #define MOTD_LINE_SIZE 128
 static char motd_text[MOTD_LINE_SIZE][CHAT_SIZE_MAX]; // Message of the day buffer [Valaris]
 
@@ -11724,6 +11746,10 @@ void pc_damage_log_clear(struct map_session_data *sd, int id)
 void pc_scdata_received(struct map_session_data *sd) {
 	pc_inventory_rentals(sd); // Needed here to remove rentals that have Status Changes after chrif_load_scdata has finished
 
+	if( pc_has_permission( sd, PC_PERM_ATTENDANCE ) && pc_attendance_enabled() && !pc_attendance_rewarded_today( sd ) ){
+		clif_ui_open( sd, OUT_UI_ATTENDANCE, pc_attendance_counter( sd ) );
+	}
+
 	sd->state.pc_loaded = true;
 
 	if (sd->state.connect_new == 0 && sd->fd) { // Character already loaded map! Gotta trigger LoadEndAck manually.
@@ -12531,6 +12557,260 @@ void pc_set_costume_view(struct map_session_data *sd) {
 		clif_changelook(&sd->bl, LOOK_ROBE, sd->status.robe);
 }
 
+struct s_attendance_period* pc_attendance_period(){
+	uint32 date = date_get(DT_YYYYMMDD);
+
+	for( struct s_attendance_period& period : attendance_periods ){
+		if( period.start <= date && period.end >= date ){
+			return &period;
+		}
+	}
+
+	return nullptr;
+}
+
+bool pc_attendance_enabled(){
+	// Check if the attendance feature is disabled
+	if( !battle_config.feature_attendance ){
+		return false;
+	}
+
+	// Check if there is a running attendance period
+	return pc_attendance_period() != nullptr;
+}
+
+static inline bool pc_attendance_rewarded_today( struct map_session_data* sd ){
+	return pc_readreg2( sd, ATTENDANCE_DATE_VAR ) >= date_get(DT_YYYYMMDD);
+}
+
+int32 pc_attendance_counter( struct map_session_data* sd ){
+	struct s_attendance_period* period = pc_attendance_period();
+
+	// No running attendance period
+	if( period == nullptr ){
+		return 0;
+	}
+
+	// Get the counter for the current period
+	int counter = pc_readreg2( sd, ATTENDANCE_COUNT_VAR );
+
+	// Check if we have a remaining counter from a previous period
+	if( counter > 0 && pc_readreg2( sd, ATTENDANCE_DATE_VAR ) < period->start ){
+		// Reset the counter to zero
+		pc_setreg2( sd, ATTENDANCE_COUNT_VAR, 0 );
+
+		return 0;
+	}
+
+	return 10 * counter + ( ( pc_attendance_rewarded_today(sd) ) ? 1 : 0 );
+}
+
+void pc_attendance_claim_reward( struct map_session_data* sd ){
+	// If the user's group does not have the permission
+	if( !pc_has_permission( sd, PC_PERM_ATTENDANCE ) ){
+		return;
+	}
+
+	// Check if the attendance feature is disabled
+	if( !pc_attendance_enabled() ){
+		return;
+	}
+
+	// Check if the user already got his reward today
+	if( pc_attendance_rewarded_today( sd ) ){
+		return;
+	}
+
+	int32 attendance_counter = pc_readreg2( sd, ATTENDANCE_COUNT_VAR );
+
+	attendance_counter += 1;
+
+	struct s_attendance_period* period = pc_attendance_period();
+
+	if( period == nullptr ){
+		return;
+	}
+
+	if( period->rewards.size() < attendance_counter ){
+		return;
+	}
+
+	pc_setreg2( sd, ATTENDANCE_DATE_VAR, date_get(DT_YYYYMMDD) );
+	pc_setreg2( sd, ATTENDANCE_COUNT_VAR, attendance_counter );
+
+	if( save_settings&CHARSAVE_ATTENDANCE )
+		chrif_save(sd, CSAVE_NORMAL);
+
+	struct s_attendance_reward& reward = period->rewards.at( attendance_counter - 1 );
+
+	struct mail_message msg;
+
+	memset( &msg, 0, sizeof( struct mail_message ) );
+
+	msg.dest_id = sd->status.char_id;
+	safestrncpy( msg.send_name, msg_txt( sd, 788 ), NAME_LENGTH );
+	safesnprintf( msg.title, MAIL_TITLE_LENGTH, msg_txt( sd, 789 ), attendance_counter );
+	safesnprintf( msg.body, MAIL_BODY_LENGTH, msg_txt( sd, 790 ), attendance_counter );
+
+	msg.item[0].nameid = reward.item_id;
+	msg.item[0].amount = reward.amount;
+	msg.item[0].identify = 1;
+
+	msg.status = MAIL_NEW;
+	msg.type = MAIL_INBOX_NORMAL;
+	msg.timestamp = time(NULL);
+
+	intif_Mail_send(0, &msg);
+
+	clif_attendence_response( sd, attendance_counter );
+}
+
+void pc_attendance_load( std::string path ){
+	YAML::Node root;
+
+	try{
+		root = YAML::LoadFile( path );
+	}catch( ... ){
+		ShowError( "pc_attendance_load: Failed to read attendance configuration file \"%s\".\n", path.c_str() );
+		return;
+	}
+
+	if( root["Attendance"] ){
+		YAML::Node attendance = root["Attendance"];
+
+		for( const auto &periodNode : attendance ){
+			if( !periodNode["Start"].IsDefined() ){
+				ShowError( "pc_attendance_load: Missing \"Start\" for period in line %d.\n", periodNode.Mark().line );
+				continue;
+			}
+
+			YAML::Node startNode = periodNode["Start"];
+
+			if( !periodNode["End"].IsDefined() ){
+				ShowError( "pc_attendance_load: Missing \"End\" for period in line %d.\n", periodNode.Mark().line );
+				continue;
+			}
+
+			YAML::Node endNode = periodNode["End"];
+
+			if( !periodNode["Rewards"].IsDefined() ){
+				ShowError( "pc_attendance_load: Missing \"Rewards\" for period in line %d.\n", periodNode.Mark().line );
+				continue;
+			}
+
+			YAML::Node rewardsNode = periodNode["Rewards"];
+
+			uint32 start = startNode.as<uint32>();
+			uint32 end = endNode.as<uint32>();
+
+			// If the period is outdated already, we do not even bother parsing
+			if( end < date_get( DT_YYYYMMDD ) ){
+				continue;
+			}
+
+			// Collision detection
+			bool collision = false;
+
+			for( struct s_attendance_period& period : attendance_periods ){
+				// Check if start is inside another period
+				if( period.start <= start && start <= period.end ){
+					ShowError( "pc_attendance_load: period start %u intersects with period %u-%u.\n", start, period.start, period.end );
+					collision = true;
+					break;
+				}
+
+				// Check if end is inside another period
+				if( period.start <= end && end <= period.end ){
+					ShowError( "pc_attendance_load: period end %u intersects with period %u-%u.\n", start, period.start, period.end );
+					collision = true;
+					break;
+				}
+			}
+
+			if( collision ){
+				continue;
+			}
+
+			struct s_attendance_period period;
+
+			period.start = start;
+			period.end = end;
+
+			for( const auto& rewardNode : rewardsNode ){
+				if( !rewardNode["Day"].IsDefined() ){
+					ShowError( "pc_attendance_load: No day defined for node in line %d.\n", rewardNode.Mark().line );
+					continue;
+				}
+
+				uint32 day = rewardNode["Day"].as<uint32>();
+
+				if( !rewardNode["ItemId"].IsDefined() ){
+					ShowError( "pc_attendance_load: No reward defined for day %d.\n", day );
+					continue;
+				}
+
+				YAML::Node itemNode = rewardNode["ItemId"];
+
+				uint16 item_id = itemNode.as<uint16>();
+
+				if( item_id == 0 || !itemdb_exists( item_id ) ){
+					ShowError( "pc_attendance_load: Unknown item ID %hu for day %d.\n", item_id, day );
+					continue;
+				}
+
+				uint16 amount;
+
+				if( rewardNode["Amount"] ){
+					amount = rewardNode["Amount"].as<uint16>();
+
+					if( amount == 0 ){
+						ShowError( "pc_attendance_load: Invalid reward count %hu for day %d. Defaulting to 1...\n", amount, day );
+						amount = 1;
+					}else if( amount > MAX_AMOUNT ){
+						ShowError( "pc_attendance_load: Reward count %hu above maximum %hu for day %d. Defaulting to %hu...\n", amount, MAX_AMOUNT, day, MAX_AMOUNT );
+						amount = MAX_AMOUNT;
+					}
+				}else{
+					amount = 1;
+				}
+
+				struct s_attendance_reward* reward = &period.rewards[day - 1];
+
+				reward->item_id = item_id;
+				reward->amount = amount;
+			}
+
+			bool missing_day = false;
+
+			for( int day = 0; day < period.rewards.size(); day++ ){
+				if( !util::map_exists( period.rewards, day ) ){
+					ShowError( "pc_attendance_load: Reward for day %d is missing.\n", day + 1 );
+					missing_day = true;
+					break;
+				}
+			}
+
+			if( missing_day ){
+				continue;
+			}
+
+			attendance_periods.push_back( period );
+		}
+	}
+}
+
+void pc_read_attendance(){
+	char path[1024];
+
+	sprintf( path, "%s/%sattendance.yml", db_path, DBPATH );
+
+	pc_attendance_load( path );
+
+	sprintf( path, "%s/%s/attendance.yml", db_path, DBIMPORT );
+
+	pc_attendance_load( path );
+}
+
 /*==========================================
  * pc Init/Terminate
  *------------------------------------------*/
@@ -12542,6 +12822,8 @@ void do_final_pc(void) {
 	ers_destroy(pc_itemgrouphealrate_ers);
 	ers_destroy(num_reg_ers);
 	ers_destroy(str_reg_ers);
+
+	attendance_periods.clear();
 }
 
 void do_init_pc(void) {
@@ -12550,6 +12832,7 @@ void do_init_pc(void) {
 
 	pc_readdb();
 	pc_read_motd(); // Read MOTD [Valaris]
+	pc_read_attendance();
 
 	add_timer_func_list(pc_invincible_timer, "pc_invincible_timer");
 	add_timer_func_list(pc_eventtimer, "pc_eventtimer");

+ 6 - 0
src/map/pc.hpp

@@ -47,6 +47,8 @@ enum sc_type : int16;
 #define JOBCHANGE3RD_VAR "jobchange_level_3rd"
 #define TKMISSIONID_VAR "TK_MISSION_ID"
 #define TKMISSIONCOUNT_VAR "TK_MISSION_COUNT"
+#define ATTENDANCE_DATE_VAR "#AttendanceDate"
+#define ATTENDANCE_COUNT_VAR "#AttendanceCounter"
 
 //Update this max as necessary. 55 is the value needed for Super Baby currently
 //Raised to 85 since Expanded Super Baby needs it.
@@ -1338,4 +1340,8 @@ bool pc_job_can_entermap(enum e_job jobid, int m, int group_lv);
 int pc_level_penalty_mod(int level_diff, uint32 mob_class, enum e_mode mode, int type);
 #endif
 
+bool pc_attendance_enabled();
+int32 pc_attendance_counter( struct map_session_data* sd );
+void pc_attendance_claim_reward( struct map_session_data* sd );
+
 #endif /* _PC_HPP_ */

+ 2 - 0
src/map/pc_groups.hpp

@@ -51,6 +51,7 @@ enum e_pc_permission : uint32 {
 	PC_PERM_ENABLE_COMMAND      = 0x01000000,
 	PC_PERM_BYPASS_STAT_ONCLONE = 0x02000000,
 	PC_PERM_BYPASS_MAX_STAT     = 0x04000000,
+	PC_PERM_ATTENDANCE          = 0x08000000,
 	//.. add other here
 	PC_PERM_ALLPERMISSION       = 0xFFFFFFFF,
 };
@@ -86,6 +87,7 @@ static const struct s_pcg_permission_name {
 	{ "command_enable",PC_PERM_ENABLE_COMMAND },
 	{ "bypass_stat_onclone",PC_PERM_BYPASS_STAT_ONCLONE },
 	{ "bypass_max_stat",PC_PERM_BYPASS_MAX_STAT },
+	{ "attendance",PC_PERM_ATTENDANCE },
 	{ "all_permission", PC_PERM_ALLPERMISSION },
 };
 

+ 2 - 1
src/map/script_constants.hpp

@@ -4451,6 +4451,7 @@
 	export_constant(DT_MONTH);
 	export_constant(DT_YEAR);
 	export_constant(DT_DAYOFYEAR);
+	export_constant(DT_YYYYMMDD);
 
 	/* instance info */
 	export_constant(IIT_ID);
@@ -4940,7 +4941,7 @@
 	export_constant(IG_COSTAMA_EGG29);
 	export_constant(IG_INK_BALL);
 	export_constant(IG_SOMETHING_CANDY_HOLDER);
-	export_constant(IG_MYSTERIOUS_EGG);
+	export_constant(IG_SHINING_EGG);
 	export_constant(IG_AGUST_LUCKY_SCROLL);
 	export_constant(IG_ELEMENT);
 	export_constant(IG_POISON);