summaryrefslogtreecommitdiff
path: root/src/map
diff options
context:
space:
mode:
authorsmokexyz <sagunkho@hotmail.com>2018-06-30 04:20:03 +0100
committerAsheraf <acheraf1998@gmail.com>2018-07-24 21:41:20 +0100
commit19aa33a5f61f0996d76d19db7dbe9d81f5daa090 (patch)
treeb54e07d42a58e34820e3131f750b56b7130cb461 /src/map
parent78c75cade7f445231f11395a6faef9b0a55568dc (diff)
downloadhercules-19aa33a5f61f0996d76d19db7dbe9d81f5daa090.tar.gz
hercules-19aa33a5f61f0996d76d19db7dbe9d81f5daa090.tar.bz2
hercules-19aa33a5f61f0996d76d19db7dbe9d81f5daa090.tar.xz
hercules-19aa33a5f61f0996d76d19db7dbe9d81f5daa090.zip
Implementation of the official Achievement System.
Source: http://ro.gnjoy.com/news/update/View.asp?seq=163&curpage=1 Script Commands - ``` achievement_progress(<ach_id>,<obj_idx>,<progress>,<incremental?>{,<char_id>}); ``` Includes an achievement_db.conf generator that reads from the item_db, mob_db (server side) and achievement_list.lub files to determine valid achievement entries based on item/monster availability. Achievements containing unsupported entries are commented out. This feature, although renewal-only in official servers, is capable of being used in pre-renewal mode on Hercules. Does not include the title system yet. A big thanks to - @MishimaHaruna for constantly reviewing. @4144 for all the support. @Asheraf for a lot of official information. Co-authored-by: "Dastgir" <dastgirp@gmail.com>
Diffstat (limited to 'src/map')
-rw-r--r--src/map/HPMmap.c1
-rw-r--r--src/map/atcommand.c6
-rw-r--r--src/map/chat.c5
-rw-r--r--src/map/chrif.c2
-rw-r--r--src/map/clif.c206
-rw-r--r--src/map/clif.h6
-rw-r--r--src/map/intif.c102
-rw-r--r--src/map/intif.h5
-rw-r--r--src/map/map.c4
-rw-r--r--src/map/map.h11
-rw-r--r--src/map/mob.c7
-rw-r--r--src/map/npc.c4
-rw-r--r--src/map/packets.h2
-rw-r--r--src/map/packets_struct.h40
-rw-r--r--src/map/party.c4
-rw-r--r--src/map/pc.c48
-rw-r--r--src/map/pc.h6
-rw-r--r--src/map/pet.c7
-rw-r--r--src/map/script.c86
-rw-r--r--src/map/unit.c1
20 files changed, 545 insertions, 8 deletions
diff --git a/src/map/HPMmap.c b/src/map/HPMmap.c
index e4640d09d..091a53311 100644
--- a/src/map/HPMmap.c
+++ b/src/map/HPMmap.c
@@ -47,6 +47,7 @@
#include "common/sysinfo.h"
#include "common/timer.h"
#include "common/utils.h"
+#include "map/achievement.h"
#include "map/atcommand.h"
#include "map/battle.h"
#include "map/battleground.h"
diff --git a/src/map/atcommand.c b/src/map/atcommand.c
index 79bd92213..6dff3f698 100644
--- a/src/map/atcommand.c
+++ b/src/map/atcommand.c
@@ -55,6 +55,7 @@
#include "map/storage.h"
#include "map/trade.h"
#include "map/unit.h"
+#include "map/achievement.h"
#include "common/cbasetypes.h"
#include "common/conf.h"
#include "common/core.h"
@@ -1421,6 +1422,10 @@ ACMD(baselevelup)
clif->updatestatus(sd, SP_BASEEXP);
clif->updatestatus(sd, SP_NEXTBASEEXP);
pc->baselevelchanged(sd);
+
+ // achievements
+ achievement->validate_stats(sd, SP_BASELEVEL, sd->status.base_level);
+
if(sd->status.party_id)
party->send_levelup(sd);
@@ -2526,6 +2531,7 @@ ACMD(param)
clif->updatestatus(sd, SP_USTR + i);
status_calc_pc(sd, SCO_FORCE);
clif->message(fd, msg_fd(fd,42)); // Stat changed.
+ achievement->validate_stats(sd, SP_STR + i, new_value); // Achievements [Smokexyz/Hercules]
} else {
if (value < 0)
clif->message(fd, msg_fd(fd,41)); // Unable to decrease the number/value.
diff --git a/src/map/chat.c b/src/map/chat.c
index 9852131be..d9b642219 100644
--- a/src/map/chat.c
+++ b/src/map/chat.c
@@ -29,6 +29,7 @@
#include "map/npc.h" // npc_event_do()
#include "map/pc.h"
#include "map/skill.h" // ext_skill_unit_onplace()
+#include "map/achievement.h"
#include "common/cbasetypes.h"
#include "common/memmgr.h"
#include "common/mmo.h"
@@ -126,6 +127,7 @@ static bool chat_createpcchat(struct map_session_data *sd, const char *title, co
pc_stop_attack(sd);
clif->createchat(sd,0); // 0 = success
clif->dispchat(cd,0);
+ achievement->validate_chatroom_create(sd); // Achievements [Smokexyz/Hercules]
return true;
}
clif->createchat(sd,1); // 1 = Room limit exceeded
@@ -181,6 +183,9 @@ static bool chat_joinchat(struct map_session_data *sd, int chatid, const char *p
cd->usersd[cd->users] = sd;
cd->users++;
+ if (cd->owner->type == BL_PC)
+ achievement->validate_chatroom_members(BL_UCAST(BL_PC, cd->owner), cd->users);
+
pc_setchatid(sd,cd->bl.id);
clif->joinchatok(sd, cd); //To the person who newly joined the list of all
diff --git a/src/map/chrif.c b/src/map/chrif.c
index cd24c5fea..609c856c6 100644
--- a/src/map/chrif.c
+++ b/src/map/chrif.c
@@ -336,6 +336,8 @@ static bool chrif_save(struct map_session_data *sd, int flag)
elemental->save(sd->ed);
if( sd->save_quest )
intif->quest_save(sd);
+ if (VECTOR_LENGTH(sd->achievement) > 0)
+ intif->achievements_save(sd);
if (sd->storage.received == true && sd->storage.save == true)
intif->send_account_storage(sd);
diff --git a/src/map/clif.c b/src/map/clif.c
index 2a2d87ccc..c71c9d38d 100644
--- a/src/map/clif.c
+++ b/src/map/clif.c
@@ -56,13 +56,14 @@
#include "map/trade.h"
#include "map/unit.h"
#include "map/vending.h"
+#include "map/achievement.h"
#include "common/HPM.h"
#include "common/cbasetypes.h"
#include "common/conf.h"
#include "common/ers.h"
#include "common/grfio.h"
#include "common/memmgr.h"
-#include "common/mmo.h" // NEW_CARTS
+#include "common/mmo.h" // NEW_CARTS, char_achievements
#include "common/nullpo.h"
#include "common/random.h"
#include "common/showmsg.h"
@@ -14786,6 +14787,7 @@ static void clif_parse_FriendsListReply(int fd, struct map_session_data *sd)
f_sd->status.friends[i].char_id = sd->status.char_id;
memcpy(f_sd->status.friends[i].name, sd->status.name, NAME_LENGTH);
clif->friendslist_reqack(f_sd, sd, 0);
+ achievement->validate_friend_add(f_sd); // Achievements [Smokexyz/Hercules]
if (battle_config.friend_auto_add) {
// Also add f_sd to sd's friendlist.
@@ -14804,6 +14806,7 @@ static void clif_parse_FriendsListReply(int fd, struct map_session_data *sd)
sd->status.friends[i].char_id = f_sd->status.char_id;
memcpy(sd->status.friends[i].name, f_sd->status.name, NAME_LENGTH);
clif->friendslist_reqack(sd, f_sd, 0);
+ achievement->validate_friend_add(sd); // Achievements [Smokexyz/Hercules]
}
}
}
@@ -20223,6 +20226,201 @@ static unsigned short clif_parse_cmd_optional(int fd, struct map_session_data *s
return cmd;
}
+/**
+ * Send all of a session's achievement information to client.
+ * Called only once on login/char-loading. (PACKET_ZC_ALL_ACH_LIST)
+ * @packet [out] 0x0A23 <ID>.W <Length>.W <ach_count>.L <total_points>.L <rank>.W <current_rank_points>.L <next_rank_points>.L <struct ach_list_info *[]>.P
+ * @param fd socket descriptor
+ * @param sd pointer to map_session_data
+ */
+static void clif_achievement_send_list(int fd, struct map_session_data *sd)
+{
+#if PACKETVER >= 20141016
+ int i = 0, count = 0, curr_exp_tmp = 0;
+ struct packet_achievement_list p = { 0 };
+
+ nullpo_retv(sd);
+
+ /* Browse through the session's achievement list and gather their values. */
+ for (i = 0; i < VECTOR_LENGTH(sd->achievement); i++) {
+ int j = 0;
+ struct achievement *a = &VECTOR_INDEX(sd->achievement, i);
+ const struct achievement_data *ad = NULL;
+
+ /* Sanity check for nonull pointers. */
+ if (a == NULL || (ad = achievement->get(a->id)) == NULL)
+ continue;
+
+ p.ach[count].ach_id = a->id;
+
+ for (j = 0; j < VECTOR_LENGTH(ad->objective); j++)
+ p.ach[count].objective[j] = a->objective[j];
+
+ if (a->completed_at) {
+ p.ach[count].completed = 1;
+ p.ach[count].completed_at = (uint32) a->completed_at;
+ p.ach[count].reward = a->rewarded_at ? 1 : 0;
+ p.total_points += ad->points;
+ }
+
+ count++;
+ }
+
+ p.packet_id = achievementListType;
+ p.packet_len = sizeof(struct ach_list_info) * count + 22;
+ p.total_achievements = count;
+ /* Determine Achievement Rank and current exp */
+ curr_exp_tmp = p.total_points;
+ for (i = 0; curr_exp_tmp && i < MAX_ACHIEVEMENT_RANKS && i < VECTOR_LENGTH(achievement->rank_exp); i++) {
+ if (curr_exp_tmp >= VECTOR_INDEX(achievement->rank_exp, i)) {
+ curr_exp_tmp -= VECTOR_INDEX(achievement->rank_exp, i);
+ p.rank++;
+
+ // Validate achievement rank type achievements.
+ achievement->validate_achievement_rank(sd, p.rank);
+ }
+ }
+ /* Determine Achievement Rank Exp */
+ p.current_rank_points = curr_exp_tmp;
+ p.next_rank_points = VECTOR_INDEX(achievement->rank_exp, p.rank);
+ /* Send payload */
+ clif->send(&p, p.packet_len, &sd->bl, SELF);
+#endif // PACKETVER >= 20141016
+}
+
+/**
+ * Sends achievement information for a single achievement.
+ * Called on objective progress updates/completion. (PACKET_ZC_ACH_UPDATE)
+ * @packet [out] 0x0A24 <ID>.W <total_points>.L <rank>.W <current_rank_points>.L <next_rank_points>.L <struct ach_list_info *>.P
+ * @param fd socket descriptor
+ * @param sd pointer to struct map_session_data
+ * @param ad const pointer to struct achievement_data from the achievement db.
+ */
+static void clif_achievement_send_update(int fd, struct map_session_data *sd, const struct achievement_data *ad)
+{
+#if PACKETVER >= 20141016
+ struct packet_achievement_update p = { 0 };
+ struct achievement *a = NULL;
+ int i = 0, points = 0, rank = 0, curr_rank_points = 0;
+
+ nullpo_retv(sd);
+ nullpo_retv(ad);
+
+ /* Get Session Achievement Data */
+ if ((a = achievement->ensure(sd, ad)) == NULL)
+ return;
+
+ /* Get total points, current rank and current rank points from the session. */
+ achievement->calculate_totals(sd, &points, NULL, &rank, &curr_rank_points);
+
+ p.packet_id = achievementUpdateType;
+
+ /* Determine Total Achievement Points */
+ p.total_points = points;
+
+ /* Determine Achievement Rank */
+ p.rank = rank;
+
+ /* Determine Achievement Rank Exp */
+ p.current_rank_points = curr_rank_points;
+ p.next_rank_points = VECTOR_INDEX(achievement->rank_exp, p.rank);
+
+ p.ach.ach_id = ad->id;
+ p.ach.completed = (uint8) achievement->check_complete(sd, ad);
+
+ for (i = 0; i < VECTOR_LENGTH(ad->objective); i++)
+ p.ach.objective[i] = a->objective[i];
+
+ p.ach.completed_at = (uint32) a->completed_at;
+
+ p.ach.reward = a->rewarded_at ? 1 : 0;
+
+ clif->send(&p, packet_len(achievementUpdateType), &sd->bl, SELF);
+
+ /* Validate rank-related achievements */
+ achievement->validate_achievement_rank(sd, rank);
+
+#endif // PACKETVER >= 20141016
+}
+
+/**
+ * Parses achievement reward packet from session.
+ * @packet [in] 0x0A25 <ach_id>.L
+ * @param fd socket descriptor.
+ * @param sd ptr to session data.
+ */
+static void clif_parse_achievement_get_reward(int fd, struct map_session_data *sd) __attribute__((nonnull(2)));
+static void clif_parse_achievement_get_reward(int fd, struct map_session_data *sd)
+{
+#if PACKETVER >= 20141016
+ int ach_id = RFIFOL(fd, 2);
+ const struct achievement_data *ad = NULL;
+ struct achievement *ach = NULL;
+
+ if (ach_id <= 0 || (ad = achievement->get(ach_id)) == NULL)
+ return;
+
+ if ((ach = achievement->ensure(sd, ad)) == NULL)
+ return;
+
+ if (achievement->check_complete(sd, ad) && ach->completed_at && ach->rewarded_at == 0) {
+ int i = 0;
+ /* Buff */
+ if (ad->rewards.bonus != NULL)
+ script->run(ad->rewards.bonus, 0, sd->bl.id, 0);
+
+ /* Give Items */
+ for (i = 0; i < VECTOR_LENGTH(ad->rewards.item); i++) {
+ struct item it = { 0 };
+ int total = 0;
+
+ it.nameid = VECTOR_INDEX(ad->rewards.item, i).id;
+ total = VECTOR_INDEX(ad->rewards.item, i).amount;
+
+ it.identify = 1;
+
+ //Check if it's stackable.
+ if (!itemdb->isstackable(it.nameid)) {
+ int j = 0;
+ for (j = 0; j < total; ++j)
+ pc->additem(sd, &it, (it.amount = 1), LOG_TYPE_SCRIPT);
+ } else {
+ pc->additem(sd, &it, (it.amount = total), LOG_TYPE_SCRIPT);
+ }
+ }
+
+ /* @TODO TitleId */
+
+ ach->rewarded_at = time(NULL);
+
+ clif->achievement_send_update(fd, sd, ad); // send update.
+
+ clif->achievement_reward_ack(fd, sd, ad);
+ }
+#endif // PACKETVER >= 20141016
+}
+
+/**
+ * Sends achievement reward collection acknowledgement to the client.
+ * @packet [out] 0x0A26 <packet_id>.W <received
+ */
+static void clif_achievement_reward_ack(int fd, struct map_session_data *sd, const struct achievement_data *ad)
+{
+#if PACKETVER >= 20141016
+ struct packet_achievement_reward_ack p = { 0 };
+
+ nullpo_retv(sd);
+ nullpo_retv(ad);
+
+ p.packet_id = achievementRewardAckType;
+ p.received = 1;
+ p.ach_id = ad->id;
+
+ clif->send(&p, packet_len(achievementRewardAckType), &sd->bl, SELF);
+#endif // PACKETVER >= 20141016
+}
+// End of Achievement System
+
/*==========================================
* RoDEX
*------------------------------------------*/
@@ -22159,6 +22357,11 @@ void clif_defaults(void)
clif->isdisguised = clif_isdisguised;
clif->navigate_to = clif_navigate_to;
clif->bl_type = clif_bl_type;
+ /* Achievement System */
+ clif->achievement_send_list = clif_achievement_send_list;
+ clif->achievement_send_update = clif_achievement_send_update;
+ clif->pAchievementGetReward = clif_parse_achievement_get_reward;
+ clif->achievement_reward_ack = clif_achievement_reward_ack;
/*------------------------
*- Parse Incoming Packet
@@ -22406,6 +22609,7 @@ void clif_defaults(void)
clif->pHotkeyRowShift = clif_parse_HotkeyRowShift;
clif->dressroom_open = clif_dressroom_open;
clif->pOneClick_ItemIdentify = clif_parse_OneClick_ItemIdentify;
+ /* Achievements [Smokexyz/Hercules] */
clif->get_bl_name = clif_get_bl_name;
/* RODEX */
clif->pRodexOpenWriteMail = clif_parse_rodex_open_write_mail;
diff --git a/src/map/clif.h b/src/map/clif.h
index 0077566b5..7a2fbaa68 100644
--- a/src/map/clif.h
+++ b/src/map/clif.h
@@ -54,6 +54,7 @@ struct skill_cd;
struct skill_unit;
struct unit_data;
struct view_data;
+struct achievement_data; // map/achievement.h
enum clif_messages;
enum rodex_add_item;
@@ -1197,6 +1198,11 @@ struct clif_interface {
bool (*isdisguised) (struct block_list* bl);
void (*navigate_to) (struct map_session_data *sd, const char* mapname, uint16 x, uint16 y, uint8 flag, bool hideWindow, uint16 mob_id);
unsigned char (*bl_type) (struct block_list *bl);
+ /* Achievement System */
+ void (*achievement_send_list) (int fd, struct map_session_data *sd);
+ void (*achievement_send_update) (int fd, struct map_session_data *sd, const struct achievement_data *ad);
+ void (*pAchievementGetReward) (int fd, struct map_session_data *sd);
+ void (*achievement_reward_ack) (int fd, struct map_session_data *sd, const struct achievement_data *ad);
/*------------------------
*- Parse Incoming Packet
*------------------------*/
diff --git a/src/map/intif.c b/src/map/intif.c
index 393058a8a..a5dc9c575 100644
--- a/src/map/intif.c
+++ b/src/map/intif.c
@@ -41,6 +41,8 @@
#include "map/quest.h"
#include "map/rodex.h"
#include "map/storage.h"
+#include "map/achievement.h"
+
#include "common/memmgr.h"
#include "common/nullpo.h"
#include "common/showmsg.h"
@@ -1730,6 +1732,98 @@ static void intif_parse_DeleteHomunculusOk(int fd)
ShowError("Homunculus data delete failure\n");
}
+/***************************************
+ * ACHIEVEMENT SYSTEM FUNCTIONS
+ ***************************************/
+/**
+ * Sends a request to the inter-server to load
+ * and send a character's achievement data.
+ * @packet [out] 0x3012 <char_id>.L
+ * @param sd pointer to map_session_data.
+ */
+static void intif_achievements_request(struct map_session_data *sd)
+{
+ nullpo_retv(sd);
+
+ if (intif->CheckForCharServer())
+ return;
+
+ WFIFOHEAD(inter_fd, 6);
+ WFIFOW(inter_fd, 0) = 0x3012;
+ WFIFOL(inter_fd, 2) = sd->status.char_id;
+ WFIFOSET(inter_fd, 6);
+}
+
+/**
+ * Handles reception of achievement data for a character from the inter-server.
+ * @packet [in] 0x3810 <packet_len>.W <char_id>.L <char_achievements[]>.P
+ * @param fd socket descriptor.
+ */
+static void intif_parse_achievements_load(int fd)
+{
+ int size = RFIFOW(fd, 2);
+ int char_id = RFIFOL(fd, 4);
+ int payload_count = (size - 8) / sizeof(struct achievement);
+ struct map_session_data *sd = map->charid2sd(char_id);
+ int i = 0;
+
+ if (sd == NULL) {
+ ShowError("intif_parse_achievements_load: Parse request for achievements received but character is offline!\n");
+ return;
+ }
+
+ if (VECTOR_LENGTH(sd->achievement) > 0) {
+ ShowError("intif_parse_achievements_load: Achievements already loaded! Possible multiple calls from the inter-server received.\n");
+ return;
+ }
+
+ VECTOR_ENSURE(sd->achievement, payload_count, 1);
+
+ for (i = 0; i < payload_count; i++) {
+ struct achievement t_ach = { 0 };
+
+ memcpy(&t_ach, RFIFOP(fd, 8 + i * sizeof(struct achievement)), sizeof(struct achievement));
+
+ if (achievement->get(t_ach.id) == NULL) {
+ ShowError("intif_parse_achievements_load: Invalid Achievement %d received from character %d. Ignoring...\n", t_ach.id, char_id);
+ continue;
+ }
+
+ VECTOR_PUSH(sd->achievement, t_ach);
+ }
+
+ clif->achievement_send_list(fd, sd);
+ sd->achievements_received = true;
+}
+
+/**
+ * Send character's achievement data to the inter-server.
+ * @packet 0x3013 <packet_len>.W <char_id>.L <achievements[]>.P
+ * @param sd pointer to map session data.
+ */
+static void intif_achievements_save(struct map_session_data *sd)
+{
+ int packet_len = 0, payload_size = 0, i = 0;
+
+ nullpo_retv(sd);
+ /* check for character server. */
+ if (intif->CheckForCharServer())
+ return;
+ /* Return if no data. */
+ if (!(payload_size = VECTOR_LENGTH(sd->achievement) * sizeof(struct achievement)))
+ return;
+
+ packet_len = payload_size + 8;
+
+ WFIFOHEAD(inter_fd, packet_len);
+ WFIFOW(inter_fd, 0) = 0x3013;
+ WFIFOW(inter_fd, 2) = packet_len;
+ WFIFOL(inter_fd, 4) = sd->status.char_id;
+ for (i = 0; i < VECTOR_LENGTH(sd->achievement); i++)
+ memcpy(WFIFOP(inter_fd, 8 + i * sizeof(struct achievement)), &VECTOR_INDEX(sd->achievement, i), sizeof(struct achievement));
+ WFIFOSET(inter_fd, packet_len);
+}
+
/**************************************
* QUESTLOG SYSTEM FUNCTIONS *
**************************************/
@@ -2799,6 +2893,7 @@ static int intif_parse(int fd)
case 0x3806: intif->pChangeNameOk(fd); break;
case 0x3807: intif->pMessageToFD(fd); break;
case 0x3808: intif->pAccountStorageSaveAck(fd); break;
+ case 0x3810: intif->pAchievementsLoad(fd); break;
case 0x3818: intif->pLoadGuildStorage(fd); break;
case 0x3819: intif->pSaveGuildStorage(fd); break;
case 0x3820: intif->pPartyCreated(fd); break;
@@ -2896,7 +2991,7 @@ void intif_defaults(void)
{
const int packet_len_table [INTIF_PACKET_LEN_TABLE_SIZE] = {
-1,-1,27,-1, -1,-1,37,-1, 7, 0, 0, 0, 0, 0, 0, 0, //0x3800-0x380f
- 0, 0, 0, 0, 0, 0, 0, 0, -1,11, 0, 0, 0, 0, 0, 0, //0x3810
+ -1, 0, 0, 0, 0, 0, 0, 0, -1,11, 0, 0, 0, 0, 0, 0, //0x3810 Achievements [Smokexyz/Hercules]
39,-1,15,15, 14,19, 7,-1, 0, 0, 0, 0, 0, 0, 0, 0, //0x3820
10,-1,15, 0, 79,23, 7,-1, 0,-1,-1,-1, 14,67,186,-1, //0x3830
-1, 0, 0,14, 0, 0, 0, 0, -1,74,-1,11, 11,-1, 0, 0, //0x3840
@@ -3001,6 +3096,9 @@ void intif_defaults(void)
intif->CheckForCharServer = CheckForCharServer;
/* */
intif->itembound_req = intif_itembound_req;
+ /* Achievement System */
+ intif->achievements_request = intif_achievements_request;
+ intif->achievements_save = intif_achievements_save;
/* parse functions */
intif->pWisMessage = intif_parse_WisMessage;
intif->pWisEnd = intif_parse_WisEnd;
@@ -3073,4 +3171,6 @@ void intif_defaults(void)
intif->pRodexCheckName = intif_parse_RodexCheckName;
/* Clan System */
intif->pRecvClanMemberAction = intif_parse_RecvClanMemberAction;
+ /* Achievement System */
+ intif->pAchievementsLoad = intif_parse_achievements_load;
}
diff --git a/src/map/intif.h b/src/map/intif.h
index c75b93201..1e98d11f8 100644
--- a/src/map/intif.h
+++ b/src/map/intif.h
@@ -145,6 +145,9 @@ struct intif_interface {
void (*request_accinfo) (int u_fd, int aid, int group_lv, char* query);
/* */
int (*CheckForCharServer) (void);
+ /* Achievement System [Smokexyz/Hercules] */
+ void(*achievements_request) (struct map_session_data *sd);
+ void(*achievements_save) (struct map_session_data *sd);
/* */
void (*pWisMessage) (int fd);
void (*pWisEnd) (int fd);
@@ -217,6 +220,8 @@ struct intif_interface {
void(*pRodexCheckName) (int fd);
/* Clan System */
void (*pRecvClanMemberAction) (int fd);
+ /* Achievements */
+ void (*pAchievementsLoad) (int fd);
};
#ifdef HERCULES_CORE
diff --git a/src/map/map.c b/src/map/map.c
index 0124a3035..ce8f4cdf5 100644
--- a/src/map/map.c
+++ b/src/map/map.c
@@ -59,6 +59,7 @@
#include "map/rodex.h"
#include "map/trade.h"
#include "map/unit.h"
+#include "map/achievement.h"
#include "common/HPM.h"
#include "common/cbasetypes.h"
#include "common/conf.h"
@@ -6134,6 +6135,7 @@ int do_final(void)
map->list_final();
vending->final();
rodex->final();
+ achievement->final();
HPM_map_do_final();
@@ -6338,6 +6340,7 @@ static void map_load_defaults(void)
pet_defaults();
path_defaults();
quest_defaults();
+ achievement_defaults();
npc_chat_defaults();
rodex_defaults();
}
@@ -6653,6 +6656,7 @@ int do_init(int argc, char *argv[])
mercenary->init(minimal);
elemental->init(minimal);
quest->init(minimal);
+ achievement->init(minimal);
npc->init(minimal);
unit->init(minimal);
bg->init(minimal);
diff --git a/src/map/map.h b/src/map/map.h
index 04525b4d5..207fef2ce 100644
--- a/src/map/map.h
+++ b/src/map/map.h
@@ -66,6 +66,16 @@ enum E_MAPSERVER_ST {
#define block_free_max 1048576
#define BL_LIST_MAX 1048576
+// The following system marks a different job ID system used by the map server,
+// which makes a lot more sense than the normal one. [Skotlex]
+// These marks the "level" of the job.
+#define JOBL_2_1 0x0100
+#define JOBL_2_2 0x0200
+#define JOBL_2 0x0300 // JOBL_2_1 | JOBL_2_2
+#define JOBL_UPPER 0x1000
+#define JOBL_BABY 0x2000
+#define JOBL_THIRD 0x4000
+
// For filtering and quick checking.
#define MAPID_BASEMASK 0x00ff
#define MAPID_UPPERMASK 0x0fff
@@ -537,6 +547,7 @@ struct flooritem_data {
};
enum status_point_types { //we better clean up this enum and change it name [Hemagx]
+ SP_NONE = -1,
SP_SPEED,SP_BASEEXP,SP_JOBEXP,SP_KARMA,SP_MANNER,SP_HP,SP_MAXHP,SP_SP, // 0-7
SP_MAXSP,SP_STATUSPOINT,SP_0a,SP_BASELEVEL,SP_SKILLPOINT,SP_STR,SP_AGI,SP_VIT, // 8-15
SP_INT,SP_DEX,SP_LUK,SP_CLASS,SP_ZENY,SP_SEX,SP_NEXTBASEEXP,SP_NEXTJOBEXP, // 16-23
diff --git a/src/map/mob.c b/src/map/mob.c
index 0dbff9211..27039490c 100644
--- a/src/map/mob.c
+++ b/src/map/mob.c
@@ -44,6 +44,7 @@
#include "map/script.h"
#include "map/skill.h"
#include "map/status.h"
+#include "map/achievement.h"
#include "common/HPM.h"
#include "common/cbasetypes.h"
#include "common/conf.h"
@@ -2188,6 +2189,10 @@ static void mob_damage(struct mob_data *md, struct block_list *src, int damage)
if (src)
mob->log_damage(md, src, damage);
md->dmgtick = timer->gettick();
+
+ // Achievements [Smokexyz/Hercules]
+ if (src != NULL && src->type == BL_PC)
+ achievement->validate_mob_damage(BL_UCAST(BL_PC, src), damage, false);
}
if (battle_config.show_mob_info&3)
@@ -2420,6 +2425,8 @@ static int mob_dead(struct mob_data *md, struct block_list *src, int type)
}
if(zeny) // zeny from mobs [Valaris]
pc->getzeny(tmpsd[i], zeny, LOG_TYPE_PICKDROP_MONSTER, NULL);
+
+ achievement->validate_mob_kill(tmpsd[i], md->db->mob_id); // Achievements [Smokexyz/Hercules]
}
}
diff --git a/src/map/npc.c b/src/map/npc.c
index 383558eaf..3a00b38ea 100644
--- a/src/map/npc.c
+++ b/src/map/npc.c
@@ -40,6 +40,7 @@
#include "map/skill.h"
#include "map/status.h"
#include "map/unit.h"
+#include "map/achievement.h"
#include "common/HPM.h"
#include "common/cbasetypes.h"
#include "common/db.h"
@@ -2272,6 +2273,9 @@ static int npc_selllist(struct map_session_data *sd, struct itemlist *item_list)
}
pc->delitem(sd, idx, entry->amount, 0, DELITEM_SOLD, LOG_TYPE_NPC);
+
+ // Achievements [Smokexyz/Hercules]
+ achievement->validate_item_sell(sd, sd->status.inventory[idx].nameid, entry->amount);
}
if (z + sd->status.zeny > MAX_ZENY && nd->master_nd == NULL)
diff --git a/src/map/packets.h b/src/map/packets.h
index d4f87f5a0..a0459e94c 100644
--- a/src/map/packets.h
+++ b/src/map/packets.h
@@ -3209,7 +3209,7 @@ packet(0x96e,-1,clif->ackmergeitems);
packet(0x0a22,3); // ZC_RECV_ROULETTE_ITEM
packet(0x0a23,-1); // ZC_ALL_ACH_LIST
packet(0x0a24,35); // ZC_ACH_UPDATE
- packet(0x0a25,6,clif->pDull/*,XXX*/); // CZ_REQ_ACH_REWARD
+ packet(0x0a25,6,clif->pAchievementGetReward, 2); // CZ_REQ_ACH_REWARD
packet(0x0a26,7); // ZC_REQ_ACH_REWARD_ACK
// changed packet sizes
packet(0x0a18,14); // ZC_ACCEPT_ENTER3
diff --git a/src/map/packets_struct.h b/src/map/packets_struct.h
index 9ae99afda..ef78f76aa 100644
--- a/src/map/packets_struct.h
+++ b/src/map/packets_struct.h
@@ -302,6 +302,11 @@ enum packet_headers {
rouletteinfoackType = 0xa1c,
roulettgenerateackType = 0xa20,
roulettercvitemackType = 0xa22,
+#if PACKETVER >= 20141016
+ achievementListType = 0xa23,
+ achievementUpdateType = 0xa24,
+ achievementRewardAckType = 0xa26,
+#endif // PACKETVER >= 20141016
#if PACKETVER >= 20150513 // [4144] 0x09f8 handling in client from 2014-10-29aRagexe and 2014-03-26cRagexeRE
questListType = 0x9f8, ///< ZC_ALL_QUEST_LIST3
#elif PACKETVER >= 20141022
@@ -2586,6 +2591,41 @@ struct PACKET_ZC_SEARCH_STORE_INFO_ACK {
struct PACKET_ZC_SEARCH_STORE_INFO_ACK_sub items[];
} __attribute__((packed));
+/* Achievement System */
+struct ach_list_info {
+ uint32 ach_id;
+ uint8 completed;
+ uint32 objective[MAX_ACHIEVEMENT_OBJECTIVES];
+ uint32 completed_at;
+ uint8 reward;
+} __attribute__((packed));
+
+struct packet_achievement_list {
+ uint16 packet_id;
+ uint16 packet_len;
+ uint32 total_achievements;
+ uint32 total_points;
+ uint16 rank;
+ uint32 current_rank_points;
+ uint32 next_rank_points;
+ struct ach_list_info ach[MAX_ACHIEVEMENT_DB];
+} __attribute__((packed));
+
+struct packet_achievement_update {
+ uint16 packet_id;
+ uint32 total_points;
+ uint16 rank;
+ uint32 current_rank_points;
+ uint32 next_rank_points;
+ struct ach_list_info ach;
+} __attribute__((packed));
+
+struct packet_achievement_reward_ack {
+ uint16 packet_id;
+ uint8 received;
+ uint32 ach_id;
+} __attribute__((packed));
+
#if !defined(sun) && (!defined(__NETBSD__) || __NetBSD_Version__ >= 600000000) // NetBSD 5 and Solaris don't like pragma pack but accept the packed attribute
#pragma pack(pop)
#endif // not NetBSD < 6 / Solaris
diff --git a/src/map/party.c b/src/map/party.c
index 9024b1c19..e4fb18c23 100644
--- a/src/map/party.c
+++ b/src/map/party.c
@@ -36,6 +36,7 @@
#include "map/pc.h"
#include "map/skill.h"
#include "map/status.h"
+#include "map/achievement.h"
#include "common/HPM.h"
#include "common/cbasetypes.h"
#include "common/memmgr.h"
@@ -189,6 +190,9 @@ static int party_create(struct map_session_data *sd, const char *name, int item,
party->fill_member(&leader, sd, 1);
intif->create_party(&leader,name,item,item2);
+
+ achievement->validate_party_create(sd); //Achievements (Smokexyz)
+
return 0;
}
diff --git a/src/map/pc.c b/src/map/pc.c
index f2cff8ab3..6f9347113 100644
--- a/src/map/pc.c
+++ b/src/map/pc.c
@@ -55,6 +55,7 @@
#include "map/skill.h"
#include "map/status.h" // struct status_data
#include "map/storage.h"
+#include "map/achievement.h"
#include "common/cbasetypes.h"
#include "common/conf.h"
#include "common/core.h" // get_svn_revision()
@@ -1053,6 +1054,11 @@ static bool pc_adoption(struct map_session_data *p1_sd, struct map_session_data
pc->skill(p1_sd, WE_CALLBABY, 1, SKILL_GRANT_PERMANENT);
pc->skill(p2_sd, WE_CALLBABY, 1, SKILL_GRANT_PERMANENT);
+ // Achievements [Smokexyz/Hercules]
+ achievement->validate_adopt(p1_sd, true); // Parent 1
+ achievement->validate_adopt(p2_sd, true); // Parent 2
+ achievement->validate_adopt(b_sd, false); // Baby
+
return true;
}
@@ -1325,6 +1331,7 @@ static bool pc_authok(struct map_session_data *sd, int login_id2, time_t expirat
sd->bg_queue.type = 0;
VECTOR_INIT(sd->script_queues);
+ VECTOR_INIT(sd->achievement); // Achievements [Smokexyz/Hercules]
VECTOR_INIT(sd->storage.item); // initialize storage item vector.
VECTOR_INIT(sd->hatEffectId);
@@ -1384,6 +1391,7 @@ static bool pc_authok(struct map_session_data *sd, int login_id2, time_t expirat
" Group '"CL_WHITE"%d"CL_RESET"').\n",
sd->status.name, sd->status.account_id, sd->status.char_id,
CONVIP(ip), sd->group_id);
+
// Send friends list
clif->friendslist_send(sd);
@@ -1474,7 +1482,6 @@ static int pc_reg_received(struct map_session_data *sd)
nullpo_ret(sd);
sd->vars_ok = true;
-
sd->change_level_2nd = pc_readglobalreg(sd,script->add_str("jobchange_level"));
sd->change_level_3rd = pc_readglobalreg(sd,script->add_str("jobchange_level_3rd"));
sd->die_counter = pc_readglobalreg(sd,script->add_str("PC_DIE_COUNTER"));
@@ -1593,6 +1600,9 @@ static int pc_reg_received(struct map_session_data *sd)
if( npc->motd ) /* [Ind/Hercules] */
script->run(npc->motd->u.scr.script, 0, sd->bl.id, npc->fake_nd->bl.id);
+ // Achievements [Smokexyz/Hercules]
+ intif->achievements_request(sd);
+
return 1;
}
@@ -4500,6 +4510,8 @@ static int pc_payzeny(struct map_session_data *sd, int zeny, enum e_log_pick_typ
sd->status.zeny -= zeny;
clif->updatestatus(sd,SP_ZENY);
+ achievement->validate_zeny(sd, -zeny); // Achievements [Smokexyz/Hercules]
+
if(!tsd) tsd = sd;
logs->zeny(sd, type, tsd, -zeny);
if( zeny > 0 && sd->state.showzeny ) {
@@ -4636,6 +4648,8 @@ static int pc_getzeny(struct map_session_data *sd, int zeny, enum e_log_pick_typ
sd->status.zeny += zeny;
clif->updatestatus(sd,SP_ZENY);
+ achievement->validate_zeny(sd, zeny); // Achievements [Smokexyz/Hercules]
+
if(!tsd) tsd = sd;
logs->zeny(sd, type, tsd, zeny);
if( zeny > 0 && sd->state.showzeny ) {
@@ -4760,6 +4774,7 @@ static int pc_additem(struct map_session_data *sd, struct item *item_data, int a
sd->status.inventory[i].amount = amount;
sd->inventory_data[i] = data;
clif->additem(sd,i,amount,0);
+
}
if( ( !itemdb->isstackable2(data) || data->flag.force_serial || data->type == IT_CASH) && !item_data->unique_id )
@@ -4767,6 +4782,8 @@ static int pc_additem(struct map_session_data *sd, struct item *item_data, int a
logs->pick_pc(sd, log_type, amount, &sd->status.inventory[i],sd->inventory_data[i]);
+ achievement->validate_item_get(sd, sd->status.inventory[i].nameid, sd->status.inventory[i].amount); // Achievements [Smokexyz/Hercules]
+
sd->weight += w;
clif->updatestatus(sd,SP_WEIGHT);
//Auto-equip
@@ -6879,11 +6896,15 @@ static int pc_checkbaselevelup(struct map_session_data *sd)
clif->misceffect(&sd->bl,0);
npc->script_event(sd, NPCE_BASELVUP); //LORDALFA - LVLUPEVENT
- if(sd->status.party_id)
+ if (sd->status.party_id)
party->send_levelup(sd);
pc->baselevelchanged(sd);
+
quest->questinfo_refresh(sd);
+
+ achievement->validate_stats(sd, SP_BASELEVEL, sd->status.base_level);
+
return 1;
}
@@ -6946,7 +6967,11 @@ static int pc_checkjoblevelup(struct map_session_data *sd)
clif->status_change(&sd->bl,SI_DEVIL1, 1, 0, 0, 0, 1); //Permanent blind effect from SG_DEVIL.
npc->script_event(sd, NPCE_JOBLVUP);
+
quest->questinfo_refresh(sd);
+
+ achievement->validate_stats(sd, SP_BASELEVEL, sd->status.job_level);
+
return 1;
}
@@ -7243,6 +7268,8 @@ static int pc_setstat(struct map_session_data *sd, int type, int val)
return -1;
}
+ achievement->validate_stats(sd, type, val); // Achievements [Smokexyz/Hercules]
+
return val;
}
@@ -7974,6 +8001,14 @@ static void pc_damage(struct map_session_data *sd, struct block_list *src, unsig
if (battle_config.prevent_logout_trigger & PLT_DAMAGE)
sd->canlog_tick = timer->gettick();
+
+ // Achievements [Smokexyz/Hercules]
+ if (src != NULL) {
+ if (src->type == BL_PC)
+ achievement->validate_pc_damage(BL_UCAST(BL_PC, src), sd, hp);
+ else if (src->type == BL_MOB)
+ achievement->validate_mob_damage(sd, hp, true);
+ }
}
/*==========================================
@@ -8124,6 +8159,8 @@ static int pc_dead(struct map_session_data *sd, struct block_list *src)
pc->setparam(ssd, SP_KILLEDRID, sd->bl.id);
npc->script_event(ssd, NPCE_KILLPC);
+ achievement->validate_pc_kill(ssd, sd); // Achievements [Smokexyz/Hercules]
+
if (battle_config.pk_mode&2) {
ssd->status.manner -= 5;
if(ssd->status.manner < 0)
@@ -9042,6 +9079,8 @@ static int pc_jobchange(struct map_session_data *sd, int class, int upper)
}
quest->questinfo_refresh(sd);
+ achievement->validate_jobchange(sd); // Achievements [Smokexyz/Hercules]
+
return 0;
}
@@ -10682,6 +10721,11 @@ static int pc_marriage(struct map_session_data *sd, struct map_session_data *dst
return -1;
sd->status.partner_id = dstsd->status.char_id;
dstsd->status.partner_id = sd->status.char_id;
+
+ // Achievements [Smokexyz/Hercules]
+ achievement->validate_marry(sd);
+ achievement->validate_marry(dstsd);
+
return 0;
}
diff --git a/src/map/pc.h b/src/map/pc.h
index 622dcf3f7..9839258e4 100644
--- a/src/map/pc.h
+++ b/src/map/pc.h
@@ -37,7 +37,7 @@
#include "common/db.h"
#include "common/ers.h" // struct eri
#include "common/hercules.h"
-#include "common/mmo.h" // JOB_*, MAX_FAME_LIST, struct fame_list, struct mmo_charstatus, NEW_CARTS
+#include "common/mmo.h" // JOB_*, MAX_FAME_LIST, struct fame_list, struct mmo_charstatus, NEW_CARTS, struct s_achievement
/**
* Defines
@@ -631,6 +631,10 @@ END_ZEROED_BLOCK;
unsigned sitstand : 1;
unsigned commands : 1;
} block_action;
+
+ /* Achievement System */
+ struct char_achievements achievement;
+ bool achievements_received;
};
#define EQP_WEAPON EQP_HAND_R
diff --git a/src/map/pet.c b/src/map/pet.c
index e544905c0..678972f14 100644
--- a/src/map/pet.c
+++ b/src/map/pet.c
@@ -23,6 +23,7 @@
#include "config/core.h" // DBPATH
#include "pet.h"
+#include "map/achievement.h"
#include "map/atcommand.h" // msg_txt()
#include "map/battle.h"
#include "map/chrif.h"
@@ -576,8 +577,8 @@ static int pet_catch_process2(struct map_session_data *sd, int target_id)
pet_catch_rate = (pet->db[i].capture + (sd->status.base_level - md->level)*30 + sd->battle_status.luk*20)*(200 - get_percentage(md->status.hp, md->status.max_hp))/100;
if(pet_catch_rate < 1) pet_catch_rate = 1;
- if(battle_config.pet_catch_rate != 100)
- pet_catch_rate = (pet_catch_rate*battle_config.pet_catch_rate)/100;
+ if(battle->bc->pet_catch_rate != 100)
+ pet_catch_rate = (pet_catch_rate*battle->bc->pet_catch_rate)/100;
if(rnd()%10000 < pet_catch_rate)
{
@@ -586,6 +587,8 @@ static int pet_catch_process2(struct map_session_data *sd, int target_id)
clif->pet_roulette(sd,1);
intif->create_pet(sd->status.account_id,sd->status.char_id,pet->db[i].class_,mob->db(pet->db[i].class_)->lv,
pet->db[i].EggID,0,pet->db[i].intimate,100,0,1,pet->db[i].jname);
+
+ achievement->validate_taming(sd, pet->db[i].class_);
}
else
{
diff --git a/src/map/script.c b/src/map/script.c
index 9a5d33604..200a43ba0 100644
--- a/src/map/script.c
+++ b/src/map/script.c
@@ -57,6 +57,7 @@
#include "map/status.h"
#include "map/storage.h"
#include "map/unit.h"
+#include "map/achievement.h"
#include "common/cbasetypes.h"
#include "common/conf.h"
#include "common/db.h"
@@ -9574,6 +9575,11 @@ static BUILDIN(successrefitem)
clif->additem(sd,i,1,0);
pc->equipitem(sd,i,ep);
clif->misceffect(&sd->bl,3);
+
+ achievement->validate_refine(sd, i, true); // Achievements [Smokexyz/Hercules]
+
+ /* The following check is exclusive to characters (possibly only whitesmiths)
+ * that create equipments and refine them to level 10. */
if(sd->status.inventory[i].refine == 10 &&
sd->status.inventory[i].card[0] == CARD0_FORGE &&
sd->status.char_id == (int)MakeDWord(sd->status.inventory[i].card[2],sd->status.inventory[i].card[3])
@@ -9611,6 +9617,9 @@ static BUILDIN(failedrefitem)
if (num > 0 && num <= ARRAYLENGTH(script->equip))
i=pc->checkequip(sd,script->equip[num-1]);
if(i >= 0) {
+ // Call before changing refine to 0.
+ achievement->validate_refine(sd, i, false);
+
sd->status.inventory[i].refine = 0;
pc->unequipitem(sd, i, PCUNEQUIPITEM_RECALC|PCUNEQUIPITEM_FORCE); //recalculate bonus
clif->refine(sd->fd,1,i,sd->status.inventory[i].refine); //notify client of failure
@@ -9658,6 +9667,9 @@ static BUILDIN(downrefitem)
clif->additem(sd,i,1,0);
pc->equipitem(sd,i,ep);
+
+ achievement->validate_refine(sd, i, false); // Achievements [Smokexyz/Hercules]
+
clif->misceffect(&sd->bl,2);
}
@@ -21246,6 +21258,78 @@ static BUILDIN(showevent)
}
/*==========================================
+ * Achievement System [Smokexyz/Hercules]
+ *-----------------------------------------*/
+/**
+ * Validates an objective index for the given achievement.
+ * Can be used for any achievement type.
+ * @command achievement_progress(<ach_id>,<obj_idx>,<progress>,<incremental?>{,<char_id>});
+ * @param aid - achievement ID
+ * @param obj_idx - achievement objective index.
+ * @param progress - objective progress towards goal.
+ * @Param incremental - (boolean) true to add the progress towards the goal,
+ * false to use the progress only as a comparand.
+ * @param account_id - (optional) character ID to perform on.
+ * @return true on success, false on failure.
+ * @push 1 on success, 0 on failure.
+ */
+static BUILDIN(achievement_progress)
+{
+ struct map_session_data *sd = script->rid2sd(st);
+ int aid = script_getnum(st, 2);
+ int obj_idx = script_getnum(st, 3);
+ int progress = script_getnum(st, 4);
+ int incremental = script_getnum(st, 5);
+ int account_id = script_hasdata(st, 6) ? script_getnum(st, 6) : 0;
+ const struct achievement_data *ad = NULL;
+
+ if ((ad = achievement->get(aid)) == NULL) {
+ ShowError("buildin_achievement_progress: Invalid achievement ID %d received.\n", aid);
+ script_pushint(st, 0);
+ return false;
+ }
+
+ if (obj_idx <= 0 || obj_idx > VECTOR_LENGTH(ad->objective)) {
+ ShowError("buildin_achievement_progress: Invalid objective index %d received. (min: %d, max: %d)\n", obj_idx, 0, VECTOR_LENGTH(ad->objective));
+ script_pushint(st, 0);
+ return false;
+ }
+
+ obj_idx--; // convert to array index.
+
+ if (progress <= 0 || progress > VECTOR_INDEX(ad->objective, obj_idx).goal) {
+ ShowError("buildin_achievement_progress: Progress exceeds goal limit for achievement id %d.\n", aid);
+ script_pushint(st, 0);
+ return false;
+ }
+
+ if (incremental < 0 || incremental > 1) {
+ ShowError("buildin_achievement_progress: Argument 4 expects boolean (0/1). provided value: %d\n", incremental);
+ script_pushint(st, 0);
+ return false;
+ }
+
+ if (script_hasdata(st, 6)) {
+ if (account_id <= 0) {
+ ShowError("buildin_achievement_progress: Invalid Account id %d provided.\n", account_id);
+ script_pushint(st, 0);
+ return false;
+ } else if ((sd = map->id2sd(account_id)) == NULL) {
+ ShowError("buildin_achievement_progress: Account with id %d was not found.\n", account_id);
+ script_pushint(st, 0);
+ return false;
+ }
+ }
+
+ if (achievement->validate(sd, aid, obj_idx, progress, incremental ? true : false))
+ script_pushint(st, progress);
+ else
+ script_pushint(st, 0);
+
+ return true;
+}
+
+/*==========================================
* BattleGround System
*------------------------------------------*/
static BUILDIN(waitingroom2bg)
@@ -25124,6 +25208,8 @@ static void script_parse_builtin(void)
BUILDIN_DEF(agitstart2,""),
BUILDIN_DEF(agitend2,""),
BUILDIN_DEF(agitcheck2,""),
+ // Achievements [Smokexyz/Hercules]
+ BUILDIN_DEF(achievement_progress, "iiii?"),
// BattleGround
BUILDIN_DEF(waitingroom2bg,"siiss?"),
BUILDIN_DEF(waitingroom2bg_single,"isiis"),
diff --git a/src/map/unit.c b/src/map/unit.c
index 2fd8d6efb..91f7e0acd 100644
--- a/src/map/unit.c
+++ b/src/map/unit.c
@@ -2765,6 +2765,7 @@ static int unit_free(struct block_list *bl, clr_type clrtype)
sd->instance = NULL;
}
VECTOR_CLEAR(sd->script_queues);
+ VECTOR_CLEAR(sd->achievement); // Achievement [Smokexyz/Hercules]
VECTOR_CLEAR(sd->storage.item);
VECTOR_CLEAR(sd->hatEffectId);
sd->storage.received = false;