Ring 70058: usable from level 1, tooltip says unlimited and no cooldown
The item_proto limit (LEVEL 30) kept new characters from using the warp ring. ring-warp/install.sh now clears it in share/conf/item_proto.txt and the player.item_proto mirror. The client proto gets the same limit through a new item_limits_server.tsv fed to locale_proto_patch, and the zh itemdesc overlay replaces the "50 uses" text. Verified on 203: a fresh level-1 character opened the warp menu four times in a row (one real warp in between), ring still in the bag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
01cc252a1d
commit
f345bd5d3b
@@ -43,7 +43,13 @@
|
||||
|
||||
`item_list_zh_overlay.txt`、`itemdesc_zh_overlay.txt` 每行是目标文件的一整行(UTF-8,第一列 VNUM),
|
||||
`build_zh_locale.py` 用它替换同 VNUM 的行(`ROW_FILES`),其余行字节不变;VNUM 不存在就报错。
|
||||
用于服务端改了用途的物品,例如 50307 小狩猎套装礼包(`tools/server/hunt-set`)换成礼盒图标和中文说明。
|
||||
用于服务端改了用途的物品,例如 50307 小狩猎套装礼包(`tools/server/hunt-set`)换成礼盒图标和中文说明,
|
||||
70058 移动戒指(`tools/server/ring-warp`)写明不限等级、无限次、无冷却。
|
||||
|
||||
## 物品限制(item_limits_server.tsv)
|
||||
|
||||
服务端 `item_proto.txt` 里改过的物品使用限制(每行 VNUM、限制类型 0、值 0、限制类型 1、值 1;0 = 无,1 = 等级),
|
||||
由 `locale_proto_patch` 写进客户端 `item_proto`,提示框显示的等级要求与服务端一致。其余字段仍逐字节校验不变。
|
||||
|
||||
## 物品说明(itemdesc_translate_zh.tsv)
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# vnum limit type 0 value 0 limit type 1 value 1 (0 = LIMIT_NONE, 1 = LIMIT_LEVEL): the item_proto.txt limits
|
||||
# changed on our server (tools/server/*), mirrored into the client proto by locale_proto_patch.
|
||||
# 70058 移动戒指: level 30 removed (tools/server/ring-warp).
|
||||
70058 0 0 0 0
|
||||
|
@@ -1,2 +1,3 @@
|
||||
# vnum name description: whole itemdesc.txt rows (build_zh_locale.py ROW_FILES)
|
||||
50307 小狩猎套装礼包 打开后按职业获得一套1级+9装备:武器、盔甲、头盔、盾牌、手镯、鞋子、项链、耳环,每件5条满值属性。需留15格背包空位,每个角色限开一次。
|
||||
70058 移动戒指 此戒指可将你传送到本国一村、二村或各主要地图,任何等级都可使用,可无限次使用,没有冷却时间。不可交易。
|
||||
|
||||
@@ -346,10 +346,12 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
|
||||
names_dir / "item_names_zh_overlay.txt")
|
||||
mob_conversion = convert_names(names_dir / "mob_names_zh.txt", mob_names,
|
||||
names_dir / "mob_names_zh_overlay.txt")
|
||||
# item_limits_server.tsv: item limits changed on our server, so the tooltip matches what it enforces.
|
||||
limits = names_dir / "item_limits_server.tsv"
|
||||
result = subprocess.run(
|
||||
[str(proto_tool), str(en / "item_proto"), str(en / "mob_proto"),
|
||||
str(item_names), str(mob_names), str(locale_dir / "item_proto"),
|
||||
str(locale_dir / "mob_proto")],
|
||||
str(locale_dir / "mob_proto")] + ([str(limits)] if limits.is_file() else []),
|
||||
check=True, capture_output=True, text=True,
|
||||
)
|
||||
report["proto"] = {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Offline CP936 name overlay for the 40250 item/mob proto format.
|
||||
// Runtime loading remains the original 40250 path and binary format.
|
||||
// An optional item limits table (vnum, limit type 0, value 0, limit type 1, value 1) mirrors the few
|
||||
// item_proto.txt limit changes made on our server (tools/server/*), so the tooltip states what the
|
||||
// server enforces; only aLimits of the listed items may change.
|
||||
#include "GameLib/StdAfx.h"
|
||||
#include "GameLib/ItemData.h"
|
||||
#include "UserInterface/StdAfx.h"
|
||||
@@ -7,6 +10,7 @@
|
||||
#include "EterBase/lzo.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
@@ -67,9 +71,53 @@ std::map<DWORD, std::string> read_names(const char* path) {
|
||||
return names;
|
||||
}
|
||||
|
||||
using Limits = std::array<CItemData::TItemLimit, CItemData::ITEM_LIMIT_MAX_NUM>;
|
||||
|
||||
std::map<DWORD, Limits> read_limits(const char* path) {
|
||||
std::map<DWORD, Limits> limits;
|
||||
if (!path) return limits;
|
||||
std::ifstream stream(path);
|
||||
if (!stream) throw std::runtime_error(std::string("cannot read ") + path);
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
unsigned long vnum, type0, type1;
|
||||
long value0, value1;
|
||||
if (std::sscanf(line.c_str(), "%lu %lu %ld %lu %ld", &vnum, &type0, &value0, &type1, &value1) != 5)
|
||||
throw std::runtime_error("bad item limits row: " + line);
|
||||
Limits& row = limits[static_cast<DWORD>(vnum)];
|
||||
row[0].bType = static_cast<BYTE>(type0);
|
||||
row[0].lValue = value0;
|
||||
row[1].bType = static_cast<BYTE>(type1);
|
||||
row[1].lValue = value1;
|
||||
}
|
||||
return limits;
|
||||
}
|
||||
|
||||
template <class Record>
|
||||
void apply_limits(Record&, const std::map<DWORD, Limits>&, std::size_t&) {}
|
||||
|
||||
template <>
|
||||
void apply_limits<Item>(Item& record, const std::map<DWORD, Limits>& limits, std::size_t& changed) {
|
||||
const auto found = limits.find(record.dwVnum);
|
||||
if (found == limits.end()) return;
|
||||
std::memcpy(record.aLimits, found->second.data(), sizeof(record.aLimits));
|
||||
++changed;
|
||||
}
|
||||
|
||||
template <class Record>
|
||||
bool limits_field(std::size_t, DWORD, const std::map<DWORD, Limits>&) { return false; }
|
||||
|
||||
template <>
|
||||
bool limits_field<Item>(std::size_t offset, DWORD vnum, const std::map<DWORD, Limits>& limits) {
|
||||
return limits.count(vnum) && offset >= offsetof(Item, aLimits) &&
|
||||
offset < offsetof(Item, aLimits) + sizeof(Item::aLimits);
|
||||
}
|
||||
|
||||
template <class Record>
|
||||
std::vector<BYTE> patch(const char* source, const char* names_path,
|
||||
std::array<DWORD, 4> key, const char* magic, bool versioned) {
|
||||
std::array<DWORD, 4> key, const char* magic, bool versioned,
|
||||
const std::map<DWORD, Limits>& limits = {}) {
|
||||
const auto input = read_file(source);
|
||||
const std::size_t count_offset = versioned ? 12 : 4;
|
||||
const std::size_t payload_offset = versioned ? 20 : 12;
|
||||
@@ -89,10 +137,12 @@ std::vector<BYTE> patch(const char* source, const char* names_path,
|
||||
std::vector<BYTE> plain(decoded.GetBuffer(), decoded.GetBuffer() + decoded.GetSize());
|
||||
const auto original = plain;
|
||||
const auto names = read_names(names_path);
|
||||
std::size_t translated = 0, missing = 0, too_long = 0;
|
||||
std::size_t translated = 0, missing = 0, too_long = 0, limited = 0;
|
||||
for (DWORD i = 0; i < count; ++i) {
|
||||
Record record;
|
||||
std::memcpy(&record, plain.data() + std::size_t(i) * sizeof(Record), sizeof(Record));
|
||||
apply_limits(record, limits, limited);
|
||||
std::memcpy(plain.data() + std::size_t(i) * sizeof(Record), &record, sizeof(Record));
|
||||
const auto found = names.find(record.dwVnum);
|
||||
if (found == names.end()) { ++missing; continue; }
|
||||
if (found->second.size() >= sizeof(record.szLocaleName)) { ++too_long; continue; }
|
||||
@@ -101,13 +151,16 @@ std::vector<BYTE> patch(const char* source, const char* names_path,
|
||||
std::memcpy(plain.data() + std::size_t(i) * sizeof(Record), &record, sizeof(Record));
|
||||
++translated;
|
||||
}
|
||||
// No item stats, VNUM, canonical name, or mob gameplay fields may change.
|
||||
// No item stats, VNUM, canonical name, or mob gameplay fields may change (but the listed item limits).
|
||||
for (DWORD i = 0; i < count; ++i) {
|
||||
const std::size_t start = std::size_t(i) * sizeof(Record);
|
||||
const std::size_t name_start = start + offsetof(Record, szLocaleName);
|
||||
const std::size_t name_end = name_start + sizeof(Record::szLocaleName);
|
||||
DWORD vnum;
|
||||
std::memcpy(&vnum, plain.data() + start + offsetof(Record, dwVnum), sizeof(vnum));
|
||||
for (std::size_t byte = start; byte < start + sizeof(Record); ++byte) {
|
||||
if ((byte < name_start || byte >= name_end) && plain[byte] != original[byte])
|
||||
if ((byte < name_start || byte >= name_end) && !limits_field<Record>(byte - start, vnum, limits) &&
|
||||
plain[byte] != original[byte])
|
||||
throw std::runtime_error("non-name proto data changed");
|
||||
}
|
||||
}
|
||||
@@ -124,19 +177,22 @@ std::vector<BYTE> patch(const char* source, const char* names_path,
|
||||
std::memcmp(verified.GetBuffer(), plain.data(), plain.size()) != 0)
|
||||
throw std::runtime_error("encoded proto failed round-trip verification");
|
||||
std::cout << magic << " count=" << count << " translated=" << translated
|
||||
<< " missing=" << missing << " too_long=" << too_long << '\n';
|
||||
<< " missing=" << missing << " too_long=" << too_long;
|
||||
if (limited) std::cout << " limits=" << limited;
|
||||
std::cout << '\n';
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 7) {
|
||||
std::cerr << "usage: locale_proto_patch item_proto mob_proto item_names_gbk mob_names_gbk out_item out_mob\n";
|
||||
if (argc != 7 && argc != 8) {
|
||||
std::cerr << "usage: locale_proto_patch item_proto mob_proto item_names_gbk mob_names_gbk out_item out_mob"
|
||||
" [item_limits]\n";
|
||||
return 2;
|
||||
}
|
||||
try {
|
||||
static CLZO lzo;
|
||||
const auto item = patch<Item>(argv[1], argv[3], item_key, "MIPX", true);
|
||||
const auto item = patch<Item>(argv[1], argv[3], item_key, "MIPX", true, read_limits(argc == 8 ? argv[7] : nullptr));
|
||||
const auto mob = patch<Mob>(argv[2], argv[4], mob_key, "MMPT", false);
|
||||
write_file(argv[5], item);
|
||||
write_file(argv[6], mob);
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
选择本国一村 / 二村、升龙谷、影悲沙漠、舒寒山、导炎大地后传送。原版能用 50 次、每次冷却 30 分钟
|
||||
(item_proto value0 / value1,记在物品 socket 0 / 1 里),用完就消失。
|
||||
|
||||
这里的 `ring_warp.quest` 去掉了次数和冷却:不再读写 socket,戒指不会消耗。在百店老板娘的商城里卖
|
||||
这里的 `ring_warp.quest` 去掉了次数和冷却:不再读写 socket,戒指不会消耗。`install.sh` 同时去掉
|
||||
`item_proto.txt` 里的 30 级限制(服务端 `CHARACTER::CanUseItem` 按它拒绝低等级角色使用),新建的角色也能用。在百店老板娘的商城里卖
|
||||
(`../npc-shops/item-mall`,1 金币)。
|
||||
|
||||
sh install.sh # 在服务器上,从本目录运行;然后重启 db 和 game
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
#!/bin/sh
|
||||
# Installs the unlimited 移动戒指 (README.md): run on the server from this directory, then restart db + game.
|
||||
# Replaces quest ring_warp with ./ring_warp.quest (GBK), recompiles all quests with make.py, and names
|
||||
# item 70058 移动戒指 on the server (item_names.txt and item_proto.locale_name; the client has its own name).
|
||||
# item 70058 移动戒指 on the server (item_names.txt and item_proto.locale_name; the client has its own name), and
|
||||
# drops its level 30 limit in item_proto.txt (CHARACTER::CanUseItem refuses it to lower characters).
|
||||
set -eu
|
||||
|
||||
SERVER=${SERVER:-/usr/metin2/server}
|
||||
LOCALE=${LOCALE:-newcibn}
|
||||
QUEST="$SERVER/share/locale/$LOCALE/quest"
|
||||
NAMES="$SERVER/share/conf/item_names.txt"
|
||||
PROTO="$SERVER/share/conf/item_proto.txt"
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
BACKUP=/root/backup-ringwarp/$(date +%Y%m%d-%H%M%S)
|
||||
export LC_ALL=C
|
||||
|
||||
mkdir -p "$BACKUP"
|
||||
cp -p "$QUEST/ring_warp.quest" "$NAMES" "$BACKUP/"
|
||||
cp -p "$QUEST/ring_warp.quest" "$NAMES" "$PROTO" "$BACKUP/"
|
||||
echo "backup: $BACKUP"
|
||||
|
||||
# A GBK trail byte 0x5C would be a backslash to qc.
|
||||
@@ -28,6 +30,14 @@ awk -F'\t' -v OFS='\t' -v name="$name" '$1 == 70058 { $2 = name; n++ } { print }
|
||||
cat "$BACKUP/item_names.new" > "$NAMES"
|
||||
mysql -uroot player -e "UPDATE item_proto SET locale_name = '$name' WHERE vnum = 70058"
|
||||
|
||||
# LIMIT_TYPE0 / LIMIT_VALUE0 (columns 15, 16): LEVEL 30 -> none. The db process loads item_proto.txt at boot
|
||||
# and mirrors it into the item_proto table.
|
||||
awk -F'\t' -v OFS='\t' '$1 == 70058 { if ($15 == "LEVEL") { $15 = "LIMIT_NONE"; $16 = 0 }; n++ } { print }
|
||||
END { if (n != 1) { print "item_proto.txt: 70058 not found" > "/dev/stderr"; exit 1 } }' \
|
||||
"$PROTO" > "$BACKUP/item_proto.new"
|
||||
cat "$BACKUP/item_proto.new" > "$PROTO"
|
||||
mysql -uroot player -e "UPDATE item_proto SET limittype0 = 0, limitvalue0 = 0 WHERE vnum = 70058 AND limittype0 = 1"
|
||||
|
||||
# make.py's shebang (/usr/local/bin/python) does not exist on 203.
|
||||
cd "$QUEST" && /usr/local/bin/python2.7 make.py > "$BACKUP/make.log" 2>&1 || { tail "$BACKUP/make.log" >&2; exit 1; }
|
||||
ls "$QUEST/object/10031/sig_use"
|
||||
|
||||
Reference in New Issue
Block a user