147 lines
7.4 KiB
Bash
Executable File
147 lines
7.4 KiB
Bash
Executable File
#!/bin/sh
|
|
# Installs one NPC shop (README.md): sh install.sh <shop dir>, where the directory has shop.conf (the shop,
|
|
# its NPC and where the NPC stands) and items.tsv (what it sells, at which price). Run on the server, then
|
|
# restart db + game. Safe to run again: prices and the shop are rewritten from items.tsv, the NPC is only
|
|
# added to a village that does not have one yet.
|
|
set -eu
|
|
|
|
[ $# -eq 1 ] || { echo "usage: sh install.sh <shop dir>" >&2; exit 2; }
|
|
SHOP_DIR=$(cd "$1" && pwd)
|
|
SERVER=${SERVER:-/usr/metin2/server}
|
|
LOCALE=${LOCALE:-newcibn}
|
|
ITEMS="$SHOP_DIR/items.tsv"
|
|
PROTO="$SERVER/share/conf/item_proto.txt"
|
|
MOB_PROTO="$SERVER/share/conf/mob_proto.txt"
|
|
MAPS="$SERVER/share/locale/$LOCALE/map"
|
|
export LC_ALL=C
|
|
|
|
# SHOP_VNUM, SHOP_NAME, NPC_VNUM, VILLAGES (one "map x y facing" line per village) and optionally NPC_NAME
|
|
# (UTF-8), the name shown over the NPC; TAB_VNUMS, the shop vnums of pages 2 and 3 when items.tsv has
|
|
# [page name] lines.
|
|
NPC_NAME=
|
|
TAB_VNUMS=
|
|
. "$SHOP_DIR/shop.conf"
|
|
MOB_NAMES="$SERVER/share/conf/mob_names.txt"
|
|
SHOP_EX="$SERVER/share/locale/$LOCALE/shop_table_ex.txt"
|
|
EX_MARK="# npc-shops/install.sh $SHOP_NAME"
|
|
BACKUP=/root/backup-npcshop/$SHOP_NAME-$(date +%Y%m%d-%H%M%S)
|
|
|
|
mkdir -p "$BACKUP"
|
|
cp -p "$PROTO" "$BACKUP/item_proto.txt"
|
|
cp -p "$MOB_PROTO" "$BACKUP/mob_proto.txt"
|
|
cp -p "$MOB_NAMES" "$BACKUP/mob_names.txt"
|
|
echo "$VILLAGES" | while read -r map x y dir; do cp -p "$MAPS/$map/npc.txt" "$BACKUP/$map.npc.txt"; done
|
|
[ ! -f "$SHOP_EX" ] || cp -p "$SHOP_EX" "$BACKUP/shop_table_ex.txt"
|
|
mysqldump -uroot player shop shop_item item_proto mob_proto > "$BACKUP/player-shop.sql"
|
|
echo "backup: $BACKUP"
|
|
|
|
# Optional stone restoration: reuse the corresponding valid +3 row for flags and
|
|
# socket family, then apply the extracted +6 values. Preserve its existing name.
|
|
if [ -f "$SHOP_DIR/stones.tsv" ]; then
|
|
awk -F'\t' -v OFS='\t' '
|
|
NR==FNR {if ($1 !~ /^#/ && NF==12) {wanted[$1]=$6; count++} next}
|
|
{line[NR]=$0; order[++n]=$1; original[$1]=$0}
|
|
END {
|
|
for (i=1;i<=n;i++) {
|
|
v=order[i]
|
|
if (v in wanted) {
|
|
split(original[v], old, "\t")
|
|
base=28330+v-28700
|
|
if (!(base in original)) exit 1
|
|
split(original[base], a, "\t")
|
|
a[1]=v; a[2]=old[2]; a[10]=1; a[11]=0; a[20]=wanted[v]
|
|
for (j=1;j<=33;j++) printf "%s%s",a[j],(j==33 ? "\n" : OFS)
|
|
found++
|
|
} else print original[v]
|
|
}
|
|
if (found!=count || count!=14) exit 1
|
|
}' "$SHOP_DIR/stones.tsv" "$PROTO" > "$BACKUP/stones.new"
|
|
cat "$BACKUP/stones.new" > "$PROTO"
|
|
awk -F'\t' '$1 !~ /^#/ && NF==12 {
|
|
printf "UPDATE item_proto SET type=%d,subtype=%d,wearflag=%d,applytype0=%d,applyvalue0=%d,applytype1=0,applyvalue1=0,applytype2=0,applyvalue2=0,value0=%d,value1=%d,value2=%d,value3=%d,value4=%d,value5=%d,shop_buy_price=0 WHERE vnum=%d;\n",$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$1
|
|
}' "$SHOP_DIR/stones.tsv" | mysql -uroot player
|
|
fi
|
|
|
|
# item_proto.txt column 10 is the price an NPC shop sells the item for (TItemTable::dwGold).
|
|
awk -F'\t' -v OFS='\t' 'NR == FNR { if ($1 !~ /^#/ && NF >= 3) { price[$1] = $3; want++ } next }
|
|
($1 in price) { $10 = price[$1]; n++ } { print }
|
|
END { if (n != want) { print "item_proto.txt: " n " of " want " items found" > "/dev/stderr"; exit 1 } }' \
|
|
"$ITEMS" "$PROTO" > "$BACKUP/item_proto.new"
|
|
cat "$BACKUP/item_proto.new" > "$PROTO"
|
|
|
|
# mob_proto.txt column 14 is ON_CLICK: the NPC only opens its shop with 1 (ON_CLICK_SHOP, trigger.cpp);
|
|
# 9010 ships with 2, which has no handler.
|
|
awk -F'\t' -v OFS='\t' -v v=$NPC_VNUM '$1 == v { $14 = 1; n++ } { print }
|
|
END { if (n != 1) { print "mob_proto.txt: NPC " v " not found" > "/dev/stderr"; exit 1 } }' \
|
|
"$MOB_PROTO" > "$BACKUP/mob_proto.new"
|
|
cat "$BACKUP/mob_proto.new" > "$MOB_PROTO"
|
|
|
|
# mob_names.txt is in the locale's code page (GB2312 for newcibn).
|
|
if [ -n "$NPC_NAME" ]; then
|
|
name=$(printf '%s' "$NPC_NAME" | iconv -f UTF-8 -t GB2312)
|
|
awk -F'\t' -v OFS='\t' -v v=$NPC_VNUM -v name="$name" '$1 == v { $2 = name; n++ } { print }
|
|
END { if (n != 1) { print "mob_names.txt: NPC " v " not found" > "/dev/stderr"; exit 1 } }' \
|
|
"$MOB_NAMES" > "$BACKUP/mob_names.new"
|
|
cat "$BACKUP/mob_names.new" > "$MOB_NAMES"
|
|
fi
|
|
|
|
# A shop with pages ([page name] lines) is an extended shop: game reads it from shop_table_ex.txt, one
|
|
# group per page, and refuses an NPC that also has a shop in the DB.
|
|
if grep -q '^\[' "$ITEMS"; then
|
|
if [ -f "$SHOP_EX" ] && ! head -1 "$SHOP_EX" | grep -qxF "$EX_MARK"; then
|
|
echo "$SHOP_EX belongs to another shop" >&2; exit 1
|
|
fi
|
|
awk -F'\t' -v npc=$NPC_VNUM -v shop="$SHOP_NAME" -v vnums="$SHOP_VNUM $TAB_VNUMS" -v mark="$EX_MARK" '
|
|
/^\[/ { name[++pages] = substr($1, 2, length($1) - 2); next }
|
|
$1 !~ /^#/ && NF >= 3 { if (!pages) exit 1; n = ++count[pages]; row[pages, n] = $1 "\t" $2 "\t" $3 }
|
|
END {
|
|
split(vnums, vnum, " ")
|
|
if (pages > 3 || !(pages in vnum)) { print "items.tsv: " pages " pages, TAB_VNUMS too short or more than 3" > "/dev/stderr"; exit 1 }
|
|
print mark
|
|
print "Group\tShopNPC\n{\n\t#--#\tnpc\tgroup"
|
|
for (p = 1; p <= pages; p++) printf "\t%d\t%d\t%s_%d\n", p, npc, shop, p
|
|
print "}"
|
|
for (p = 1; p <= pages; p++) {
|
|
if (count[p] > 39) { print "items.tsv: page " p " has more than 39 items" > "/dev/stderr"; exit 1 }
|
|
# Rows are read in key order (a std::map), so the keys are zero-padded.
|
|
printf "Group\t%s_%d\n{\n\tVnum\t%d\n\tName\t%s\n\tGroup\titems\n\t{\n\t\t#--#\tvnum\tcount\tprice\n", shop, p, vnum[p], name[p]
|
|
for (i = 1; i <= count[p]; i++) printf "\t\t%02d\t%s\n", i, row[p, i]
|
|
print "\t}\n}"
|
|
}
|
|
}' "$ITEMS" > "$BACKUP/shop_table_ex.utf8"
|
|
# Page names go to the client in the locale's code page.
|
|
iconv -f UTF-8 -t GB2312 "$BACKUP/shop_table_ex.utf8" > "$SHOP_EX"
|
|
vnums=$(echo $SHOP_VNUM $TAB_VNUMS | tr ' ' ',')
|
|
{
|
|
awk -F'\t' '$1 !~ /^#/ && NF >= 3 { printf "UPDATE item_proto SET gold = %d WHERE vnum = %d;\n", $3, $1 }' "$ITEMS"
|
|
echo "UPDATE mob_proto SET on_click = 1 WHERE vnum = $NPC_VNUM;"
|
|
echo "DELETE FROM shop_item WHERE shop_vnum IN ($vnums);"
|
|
echo "DELETE FROM shop WHERE vnum IN ($vnums);"
|
|
} | mysql -uroot player
|
|
echo "$SHOP_EX: $(grep -c '^Group' "$SHOP_EX") groups"
|
|
else
|
|
{
|
|
awk -F'\t' '$1 !~ /^#/ && NF >= 3 { printf "UPDATE item_proto SET gold = %d WHERE vnum = %d;\n", $3, $1 }' "$ITEMS"
|
|
echo "UPDATE mob_proto SET on_click = 1 WHERE vnum = $NPC_VNUM;"
|
|
echo "REPLACE INTO shop (vnum, name, npc_vnum) VALUES ($SHOP_VNUM, '$SHOP_NAME', $NPC_VNUM);"
|
|
echo "DELETE FROM shop_item WHERE shop_vnum = $SHOP_VNUM;"
|
|
awk -F'\t' -v shop=$SHOP_VNUM '$1 !~ /^#/ && NF >= 3 { printf "INSERT INTO shop_item (shop_vnum, item_vnum, count) VALUES (%d, %d, %d);\n", shop, $1, $2 }' "$ITEMS"
|
|
} | mysql -uroot player
|
|
fi
|
|
|
|
echo "$VILLAGES" | while read -r map x y dir; do
|
|
npc="$MAPS/$map/npc.txt"
|
|
if awk -F'\t' -v v=$NPC_VNUM '$11 == v { found = 1 } END { exit !found }' "$npc"; then
|
|
awk -F'\t' -v OFS='\t' -v v=$NPC_VNUM -v x="$x" -v y="$y" -v dir="$dir" '$11==v {$2=x; $3=y; $7=dir} {print}' "$npc" > "$BACKUP/$map.npc.new"
|
|
cat "$BACKUP/$map.npc.new" > "$npc"
|
|
echo "$map: NPC $NPC_VNUM positioned at $x,$y"
|
|
else
|
|
[ -z "$(tail -c 1 "$npc")" ] || echo >> "$npc"
|
|
printf 'm\t%d\t%d\t0\t0\t0\t%d\t1m\t100\t1\t%d\n' "$x" "$y" "$dir" "$NPC_VNUM" >> "$npc"
|
|
echo "$map: NPC $NPC_VNUM at $x,$y"
|
|
fi
|
|
done
|
|
|
|
grep -q '^\[' "$ITEMS" || mysql -uroot player -N -e "SELECT COUNT(*) FROM shop_item WHERE shop_vnum = $SHOP_VNUM"
|
|
echo "done; restart db and game (README.md)"
|