diff --git a/CHANGELOG.md b/CHANGELOG.md
index d46480ac5b9..ddcf69743fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -792,10 +792,20 @@ All these changes only affect Renewal. Pre-renewal is unchanged.
- The `is_quest` argument to `pc->gainexp()` has been changed to a `flags` bitmask enum, in order to allow expansion to different flags. (#3279)
-## [v2024.08] `August 2024`
+## [v2024.09] `September 2024`
### Added
+- Implemented the script command `getunitparam()` to query values defined in `unit_parameters_db.conf`, and the related `UNIT_PARAM_*` constants. See the `script_commands.txt` documentation for usage details. (#3323)
+- Added validation of the name length for configuration entries added through the HPM `addBattleConf()`, `addLoginConf()`, `addCharConf()`, `addCharInterConf()`, `addLogConf()`, `addScriptConf()` methods, to prevent silent truncation. (#3324)
+
+### Fixed
+
+- Fixed an issue causing item-granted skills that were overriding an existing skill level, not to be correctly cleared when unequipping the item. (#3322)
+- Fixed previously plagiarized skills re-appearing on subsequent logins due to the related script variables not getting cleared properly. (#3325)
+
+## [v2024.08] `August 2024`
+
### Changed
- Converted packets `CHARLOGIN_ONLINE_ACCOUNTS`, `MAPCHAR_AUTH_REQ`, `CHARLOGIN_SET_ACCOUNT_ONLINE` to the struct format. (#3304, #3312, #3314)
@@ -3930,6 +3940,7 @@ Note: everything included in this release is part of PR #3198 which consists of
- New versioning scheme and project changelogs/release notes (#1853)
[Unreleased]: https://github.com/HerculesWS/Hercules/compare/stable...master
+[v2024.09]: https://github.com/HerculesWS/Hercules/compare/v2024.08...v2024.09
[v2024.08]: https://github.com/HerculesWS/Hercules/compare/v2024.06...v2024.08
[v2024.06]: https://github.com/HerculesWS/Hercules/compare/v2024.05...v2024.06
[v2024.05]: https://github.com/HerculesWS/Hercules/compare/v2024.04...v2024.05
diff --git a/doc/constants_pre-re.md b/doc/constants_pre-re.md
index 25ab8f2ae30..7c46aba797a 100644
--- a/doc/constants_pre-re.md
+++ b/doc/constants_pre-re.md
@@ -4880,7 +4880,7 @@
### Server defines
- `PACKETVER`: 20190530
-- `HERCULES_VERSION`: 202408000
+- `HERCULES_VERSION`: 202409000
- `MAX_LEVEL`: 175
- `MAX_STORAGE`: 600
- `MAX_GUILD_STORAGE`: 500
@@ -5870,6 +5870,14 @@
- `HOMINFO_RENAME`: 5
- `HOMINFO_LEVEL`: 6
+### getunitparam param-types
+
+- `UNIT_PARAM_NAME`: 0
+- `UNIT_PARAM_NATHEAL_WEIGHT_RATE`: 1
+- `UNIT_PARAM_MAX_ASPD`: 2
+- `UNIT_PARAM_MAX_HP`: 3
+- `UNIT_PARAM_MAX_STATS`: 4
+
### Renewal
- `RENEWAL`: 0
diff --git a/doc/constants_re.md b/doc/constants_re.md
index 40dc94ff76c..7f116999fb3 100644
--- a/doc/constants_re.md
+++ b/doc/constants_re.md
@@ -4878,7 +4878,7 @@
### Server defines
- `PACKETVER`: 20190530
-- `HERCULES_VERSION`: 202408000
+- `HERCULES_VERSION`: 202409000
- `MAX_LEVEL`: 175
- `MAX_STORAGE`: 600
- `MAX_GUILD_STORAGE`: 500
@@ -5868,6 +5868,14 @@
- `HOMINFO_RENAME`: 5
- `HOMINFO_LEVEL`: 6
+### getunitparam param-types
+
+- `UNIT_PARAM_NAME`: 0
+- `UNIT_PARAM_NATHEAL_WEIGHT_RATE`: 1
+- `UNIT_PARAM_MAX_ASPD`: 2
+- `UNIT_PARAM_MAX_HP`: 3
+- `UNIT_PARAM_MAX_STATS`: 4
+
### Renewal
- `RENEWAL`: 1
diff --git a/doc/script_commands.txt b/doc/script_commands.txt
index 1c4bb8aa3ab..939ea863f4f 100644
--- a/doc/script_commands.txt
+++ b/doc/script_commands.txt
@@ -3101,6 +3101,37 @@ Examples:
---------------------------------------
+*getunitparam({, {, , }})
+
+Thi will return the requested parameter's value or fill the required arrays in
+the case of UNIT_PARAM_MAX_HP.
+
+:
+- UNIT_PARAM_NAME ^= name of unit_parameters_db.conf entry used by class.
+- UNIT_PARAM_NATHEAL_WEIGHT_RATE ^= NaturalHealWeightRate value
+- UNIT_PARAM_MAX_ASPD ^= MaxASPD
+- UNIT_PARAM_MAX_HP ^= MaxHP
+- UNIT_PARAM_MAX_STATS ^= MaxStats
+
+ can be -1 if attached players class is desired, such as in the case of UNIT_PARAM_MAX_HP,
+where is mandatory.
+
+Examples:
+
+// Outputs the possible maximum ASPD of attached player.
+ mesf("MAX_ASPD: %d", getunitparam(UNIT_PARAM_MAX_ASPD));
+
+// Outputs the possible maximum stats of Job_Baby.
+ mesf("MAX_STATS: %d", getunitparam(UNIT_PARAM_MAX_STATS, Job_Baby));
+
+// Saves the entries for MAXHP per level ranges of JOB_SUPER_NOVICE into the given arrays and prints them.
+ .@count = getunitparam(UNIT_PARAM_MAX_ASPD, JOB_SUPER_NOVICE, .@max_lv, .@max_hp);
+ for (.@i = 0; .@i < .@count; ++.@i) {
+ mesf("max_lv: %d, max_hp: %d", .@max_lv[.@i], .@max_hp[.@i]);
+ }
+
+---------------------------------------
+
*sit({""})
*stand({""})
diff --git a/src/common/HPM.c b/src/common/HPM.c
index 9608eec218e..4aa80c0ec38 100644
--- a/src/common/HPM.c
+++ b/src/common/HPM.c
@@ -451,6 +451,11 @@ static bool hplugins_addconf(unsigned int pluginID, enum HPluginConfType type, c
return false;
}
+ if (strnlen(name, HPM_ADDCONF_LENGTH) >= HPM_ADDCONF_LENGTH) {
+ ShowError("HPM->addConf:%s: config '%s' name/path is too long. Maximum is %d characters (see #define HPM_ADDCONF_LENGTH). Skipping it.\n", HPM->pid2name(pluginID), name, HPM_ADDCONF_LENGTH - 1);
+ return false;
+ }
+
ARR_FIND(0, VECTOR_LENGTH(HPM->config_listeners[type]), i, strcmpi(name, VECTOR_INDEX(HPM->config_listeners[type], i).key) == 0);
if (i != VECTOR_LENGTH(HPM->config_listeners[type])) {
ShowError("HPM->addConf:%s: duplicate '%s', already in use by '%s'!",
diff --git a/src/common/HPMi.h b/src/common/HPMi.h
index d7083bbf8ff..a44984495f2 100644
--- a/src/common/HPMi.h
+++ b/src/common/HPMi.h
@@ -35,6 +35,8 @@ struct map_session_data;
struct hplugin_data_store;
#define HPM_VERSION "1.2"
+
+// Maximum length of the configuration path for configs added with add*Conf
#define HPM_ADDCONF_LENGTH 40
struct hplugin_info {
diff --git a/src/config/core.h b/src/config/core.h
index 657f29bbe65..83bd05a1c97 100644
--- a/src/config/core.h
+++ b/src/config/core.h
@@ -22,7 +22,7 @@
#define CONFIG_CORE_H
/// Hercules version. From tag vYYYY.MM(+PPP) -> YYYYMMPPP
-#define HERCULES_VERSION 202408000
+#define HERCULES_VERSION 202409000
/// Max number of items on @autolootid list
#define AUTOLOOTITEM_SIZE 10
diff --git a/src/map/pc.c b/src/map/pc.c
index f10b8282e98..65fe219b491 100644
--- a/src/map/pc.c
+++ b/src/map/pc.c
@@ -1659,7 +1659,8 @@ static void pc_calc_skilltree_clear(struct map_session_data *sd)
for (i = 0; i < MAX_SKILL_DB; i++) {
if (sd->status.skill[i].flag == SKILL_FLAG_PLAGIARIZED || sd->status.skill[i].flag == SKILL_FLAG_PERM_GRANTED
- || sd->status.skill[i].id == sd->cloneskill_id || sd->status.skill[i].id == sd->reproduceskill_id) //Don't touch these
+ || (sd->cloneskill_id != 0 && sd->status.skill[i].id == sd->cloneskill_id)
+ || (sd->reproduceskill_id != 0 && sd->status.skill[i].id == sd->reproduceskill_id)) //Don't touch these
continue;
sd->status.skill[i].id = 0; //First clear skills.
/* permanent skills that must be re-checked */
@@ -1695,10 +1696,17 @@ static int pc_calc_skilltree(struct map_session_data *sd)
pc->calc_skilltree_clear(sd);
for (int i = 0; i < MAX_SKILL_DB; i++) {
- if ((sd->status.skill[i].flag >= SKILL_FLAG_REPLACED_LV_0 && sd->status.skill[i].id != sd->cloneskill_id && sd->status.skill[i].id != sd->reproduceskill_id)
- || sd->status.skill[i].flag == SKILL_FLAG_TEMPORARY) {
+ if (sd->status.skill[i].flag >= SKILL_FLAG_REPLACED_LV_0) {
+ bool is_cloneskill = sd->cloneskill_id != 0 && sd->status.skill[i].id == sd->cloneskill_id;
+ bool is_reproduceskill = sd->reproduceskill_id != 0 && sd->status.skill[i].id == sd->reproduceskill_id;
+ if (is_cloneskill || is_reproduceskill)
+ continue; // Plagiarized and Reproduce Skills are kept.
+
// Restore original level of skills after deleting earned skills.
- sd->status.skill[i].lv = (sd->status.skill[i].flag == SKILL_FLAG_TEMPORARY) ? 0 : sd->status.skill[i].flag - SKILL_FLAG_REPLACED_LV_0;
+ sd->status.skill[i].lv = sd->status.skill[i].flag - SKILL_FLAG_REPLACED_LV_0;
+ sd->status.skill[i].flag = SKILL_FLAG_PERMANENT;
+ } else if (sd->status.skill[i].flag == SKILL_FLAG_TEMPORARY) {
+ sd->status.skill[i].lv = 0;
sd->status.skill[i].flag = SKILL_FLAG_PERMANENT;
}
}
diff --git a/src/map/script.c b/src/map/script.c
index 0024462c2a7..359299bc31e 100644
--- a/src/map/script.c
+++ b/src/map/script.c
@@ -28628,6 +28628,120 @@ static BUILDIN(mestipbox)
return true;
}
+
+/**
+ * Returns a units 's values
+ *
+ * NOTE: UNIT_PARAM_MAX_HP needs two arrays.
+ *
+ * getunitparam({, {, , }})
+ */
+static BUILDIN(getunitparam)
+{
+ int class = -1;
+ if (script_hasdata(st, 3)) {
+ class = script_getnum(st, 3);
+ if (class != -1) {
+ if (!pc->db_checkid(class)) {
+ ShowError("buildin_getunitparam: invalid class (%d)\n", class);
+ st->state = END;
+ return false;
+ }
+ class = pc->class2idx(class);
+ }
+ }
+
+ struct map_session_data *sd = NULL;
+ if (class == -1) {
+ sd = script_rid2sd(st);
+ if (sd == NULL) {
+ ShowError("buildin_getunitparam: No player attached, but class == -1.\n");
+ return false;
+ }
+ class = pc->class2idx(sd->status.class);
+ }
+
+ struct s_unit_params *entry = status->dbs->unit_params[class];
+ int param = script_getnum(st, 2);
+ switch (param) {
+ case UNIT_PARAM_NAME:
+ script_pushconststr(st, entry->name);
+ break;
+ case UNIT_PARAM_NATHEAL_WEIGHT_RATE:
+ script_pushint(st, entry->natural_heal_weight_rate);
+ break;
+ case UNIT_PARAM_MAX_ASPD:
+ script_pushint(st, (2000 - entry->max_aspd) / 10); // max_aspd is actually min_amotion :)
+ break;
+ case UNIT_PARAM_MAX_HP: {
+ if (!script_hasdata(st, 4) || !script_hasdata(st, 5)) {
+ ShowError("buildin_getunitparam: UNIT_PARAM_MAXP_HP requires 4 parameters: , , , \n");
+ st->state = END;
+ return false;
+ }
+
+ struct script_data *maxhp_maxlvls = script_getdata(st, 4);
+ struct script_data *maxhp_values = script_getdata(st, 5);
+ if (!data_isreference(maxhp_maxlvls) || reference_toconstant(maxhp_maxlvls)) {
+ ShowError("buildin_getunitparam: argument must be reference and not a reference to constant\n");
+ script->reportdata(maxhp_maxlvls);
+ st->state = END;
+ return false;
+ }
+ if (!data_isreference(maxhp_values) || reference_toconstant(maxhp_values)) {
+ ShowError("buildin_getunitparam: argument must be reference and not a reference to constant\n");
+ script->reportdata(maxhp_values);
+ st->state = END;
+ return false;
+ }
+
+ const char *maxhp_maxlvls_varname = reference_getname(maxhp_maxlvls);
+ const char *maxhp_values_varname = reference_getname(maxhp_values);
+ if (!is_int_variable(maxhp_maxlvls_varname)) {
+ ShowError("buildin_getunitparam: argument must be of integer type\n");
+ script->reportdata(maxhp_maxlvls);
+ st->state = END;
+ return false;
+ }
+ if (!is_int_variable(maxhp_values_varname)) {
+ ShowError("buildin_getunitparam: argument must be of integer type\n");
+ script->reportdata(maxhp_values);
+ st->state = END;
+ return false;
+ }
+
+ if (not_server_variable(*maxhp_maxlvls_varname) || not_server_variable(*maxhp_values_varname)) {
+ if (sd == NULL) {
+ sd = script->rid2sd(st);
+ if (sd == NULL)
+ return false; // player variable but no player attached
+ }
+ }
+
+ int varid1 = reference_getid(maxhp_maxlvls);
+ int varid2 = reference_getid(maxhp_values);
+ int count = entry->maxhp_size;
+ for (int i = 0; i < count; i++) {
+ script->set_reg(st, sd, reference_uid(varid1, i), maxhp_maxlvls_varname, (const void *)h64BPTRSIZE(entry->maxhp[i].max_level),
+ reference_getref(maxhp_maxlvls));
+ script->set_reg(st, sd, reference_uid(varid2, i), maxhp_values_varname, (const void *)h64BPTRSIZE(entry->maxhp[i].value),
+ reference_getref(maxhp_values));
+ }
+ script_pushint(st, count);
+ break;
+ }
+ case UNIT_PARAM_MAX_STATS:
+ script_pushint(st, entry->max_stats);
+ break;
+ default:
+ ShowError("buildin_getunitparam: Received invalid param: %d\n", param);
+ st->state = END;
+ return false;
+ }
+
+ return true;
+}
+
/**
* Adds a built-in script function.
*
@@ -29507,6 +29621,7 @@ static void script_parse_builtin(void)
BUILDIN_DEF(mesurl, "ss??"),
BUILDIN_DEF(mestipbox, "si"),
+ BUILDIN_DEF(getunitparam, "i???"),
};
int i, len = ARRAYLENGTH(BUILDIN);
RECREATE(script->buildin, char *, script->buildin_count + len); // Pre-alloc to speed up
@@ -30490,6 +30605,13 @@ static void script_hardcoded_constants(void)
script->set_constant("HOMINFO_RENAME", HOMINFO_RENAME, false, false);
script->set_constant("HOMINFO_LEVEL", HOMINFO_LEVEL, false, false);
+ script->constdb_comment("getunitparam param-types");
+ script->set_constant("UNIT_PARAM_NAME", UNIT_PARAM_NAME, false, false);
+ script->set_constant("UNIT_PARAM_NATHEAL_WEIGHT_RATE", UNIT_PARAM_NATHEAL_WEIGHT_RATE, false, false);
+ script->set_constant("UNIT_PARAM_MAX_ASPD", UNIT_PARAM_MAX_ASPD, false, false);
+ script->set_constant("UNIT_PARAM_MAX_HP", UNIT_PARAM_MAX_HP, false, false);
+ script->set_constant("UNIT_PARAM_MAX_STATS", UNIT_PARAM_MAX_STATS, false, false);
+
script->constdb_comment("autospell db constants");
script->set_constant2("HALF_AUTOSPELL_LEVEL", HALF_AUTOSPELL_LEVEL, false, false);
diff --git a/src/map/skill.c b/src/map/skill.c
index 8e022cc884a..f8a8feb7a1f 100644
--- a/src/map/skill.c
+++ b/src/map/skill.c
@@ -3704,12 +3704,13 @@ static int skill_attack(int attack_type, struct block_list *src, struct block_li
switch(can_copy(tsd, copy_skill)) {
case 1: // Plagiarism
{
- pc->clear_existing_cloneskill(tsd, false);
-
lv = min(skill_lv, pc->checkskill(tsd, RG_PLAGIARISM));
- if (learned_lv > lv)
+ if (learned_lv > lv) {
+ pc->clear_existing_cloneskill(tsd, true);
break; // [Aegis] can't overwrite skill of higher level, but will still remove previously copied skill.
+ }
+ pc->clear_existing_cloneskill(tsd, false);
tsd->cloneskill_id = copy_skill;
pc_setglobalreg(tsd, script->add_variable("CLONE_SKILL"), copy_skill);
pc_setglobalreg(tsd, script->add_variable("CLONE_SKILL_LV"), lv);
@@ -3726,12 +3727,13 @@ static int skill_attack(int attack_type, struct block_list *src, struct block_li
case 2: // Reproduce
{
lv = sc ? sc->data[SC__REPRODUCE]->val1 : 1;
- pc->clear_existing_reproduceskill(tsd, false);
-
lv = min(lv, skill->get_max(copy_skill));
- if (learned_lv > lv)
+ if (learned_lv > lv) {
+ pc->clear_existing_reproduceskill(tsd, true);
break; // unconfirmed, but probably the same behavior as for RG_PLAGIARISM
+ }
+ pc->clear_existing_reproduceskill(tsd, false);
tsd->reproduceskill_id = copy_skill;
pc_setglobalreg(tsd, script->add_variable("REPRODUCE_SKILL"), copy_skill);
pc_setglobalreg(tsd, script->add_variable("REPRODUCE_SKILL_LV"), lv);
diff --git a/src/map/status.h b/src/map/status.h
index 9d1f1687255..c491fb4a641 100644
--- a/src/map/status.h
+++ b/src/map/status.h
@@ -1351,6 +1351,14 @@ struct s_maxhp_entry {
int value; ///< The actual max hp value
};
+enum e_unit_params {
+ UNIT_PARAM_NAME,
+ UNIT_PARAM_NATHEAL_WEIGHT_RATE,
+ UNIT_PARAM_MAX_ASPD,
+ UNIT_PARAM_MAX_HP,
+ UNIT_PARAM_MAX_STATS,
+};
+
struct s_unit_params {
char name[SCRIPT_VARNAME_LENGTH]; ///< group name as defined in conf