#!/bin/sh
# =============================================================================
#  m2-rates -- server-wide experience / item drop / yang rates, mt2009 edition.
#
#  Same spool protocol as r40250's m2-rates (panel writes $SPOOL/request,
#  m2-supervise polls, restarts the cores, publishes $SPOOL/rates.status), and
#  m2-supervise is byte-for-byte the r40250 one, so the subcommands and their
#  exit codes are kept exactly.  What differs is where the numbers live.
#
#  On r40250 a rate is a rewrite of mob_proto.txt and the three drop tables.
#  This engine keeps its protos in the database (PROTO_FROM_DB) and, more to
#  the point, already has server-wide multipliers of its own: CHARACTER_MANAGER
#  holds m_iMobExpRate / m_iMobItemRate / m_iMobGoldAmountRate and the "_buyer"
#  premium twins, char_battle.cpp and item_manager.cpp multiply by them, and
#  CQuestManager::SetEventFlag() sets them from the event flags mob_exp,
#  mob_item, mob_gold (+ mob_exp_buyer, mob_item_buyer, mob_gold_buyer).  Event
#  flags are rows of player.quest with dwPID = 0 (ClientManagerEventFlag.cpp:
#  loaded at db-core boot, pushed to every game core at setup, REPLACEd on
#  change).
#
#  So the panel writes those six rows itself -- it has the database, this
#  container has no client for it -- and what is left for this side is what
#  the supervisor already does: notice the request, restart the cores so the
#  db core re-reads the flags, and tell the panel how it went.  `apply' has
#  nothing to rewrite and says so; it still keeps $STATE_DIR/wanted so that
#  `publish' can report the numbers the way the r40250 script does.
#
#  When a player is in game the classic panel does not even come here: the
#  web_admin quest sets the flags live through game.set_event_flag() and the
#  panel only falls back to this restart when nothing in game answered.
#
#  Subcommands
#      prepare        as root, at container start: spool ownership
#      apply          nothing to rewrite here; records the result
#      poll           new request from the panel?  exit 10 = yes, restart
#      publish [S M]  write the status file the panel reads; with no argument
#                     it republishes the result of the last apply
# =============================================================================
set -u
LC_ALL=C
export LC_ALL

VAR_DIR="${M2_VAR_DIR:-/opt/metin2/var}"
SPOOL="${M2_RATES_SPOOL:-/opt/m2spool}"

STATE_DIR="$VAR_DIR/rates"
WANTED="$STATE_DIR/wanted"          # what the operator asked for, kept forever
LAST_ID="$STATE_DIR/last-id"        # the request we have already dealt with
RESULT="$STATE_DIR/last-result"     # how the last apply went

REQUEST="$SPOOL/request"
STATUS="$SPOOL/rates.status"

LOG="${M2_RATES_LOG:-/var/log/m2rates.log}"
if ! { : >>"$LOG"; } 2>/dev/null; then
    LOG="$STATE_DIR/apply.log"
    mkdir -p "$STATE_DIR" 2>/dev/null
    { : >>"$LOG"; } 2>/dev/null || LOG=/dev/null
fi

RATE_EXP=100
RATE_DROP=100
RATE_YANG=100

say()  { printf '%s [rates] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%SZ')" "$1"; }
note() { say "$1"; say "$1" >>"$LOG" 2>/dev/null; }
fail() { note "  [ XX ] $1"; }
step() { note "== $1"; }

sane_rate() {
    _v="${1:-}"
    case "${_v:-x}" in ''|*[!0-9]*) _v=100 ;; esac
    [ "$_v" -lt 1 ] && _v=100
    [ "$_v" -gt 10000 ] && _v=10000
    printf '%s' "$_v"
}

kv() {
    [ -f "$1" ] || return 0
    sed -n "s/^$2=//p" "$1" 2>/dev/null | head -1
}

read_wanted() {
    RATE_EXP=$(sane_rate  "$(kv "$WANTED" exp)")
    RATE_DROP=$(sane_rate "$(kv "$WANTED" drop)")
    RATE_YANG=$(sane_rate "$(kv "$WANTED" yang)")
}

# Written to a temporary file and renamed: the panel may read it at any
# moment, and it writes this same file from its own container as its own
# user, so replacing it wholesale is the only reliable way to update it.
write_status() {
    _tmp="$STATUS.new.$$"
    { printf 'state=%s\n' "$1"
      printf 'time=%s\n' "$(date +%s 2>/dev/null)"
      printf 'exp=%s\ndrop=%s\nyang=%s\n' "$RATE_EXP" "$RATE_DROP" "$RATE_YANG"
      printf 'message=%s\n' "$2"
    } > "$_tmp" 2>/dev/null || { rm -f "$_tmp"; return 1; }
    chmod 0660 "$_tmp" 2>/dev/null
    mv "$_tmp" "$STATUS" 2>/dev/null || { rm -f "$_tmp"; return 1; }
    return 0
}

remember_result() { # remember_result STATE MESSAGE
    mkdir -p "$STATE_DIR" 2>/dev/null
    { printf 'state=%s\n' "$1"; printf 'message=%s\n' "$2"; } > "$RESULT.new" 2>/dev/null \
        && mv "$RESULT.new" "$RESULT" 2>/dev/null
    return 0
}

# ---------------------------------------------------------------------------
#  prepare -- run as root from the entrypoint, before privileges are dropped
# ---------------------------------------------------------------------------
# Only the spool needs root here: a fresh named volume is an empty root-owned
# directory, and both service accounts have to be able to write into it.
# Nothing in the image is rewritten on this engine, so no table changes owner.
# ---------------------------------------------------------------------------
cmd_prepare() {
    mkdir -p "$STATE_DIR" 2>/dev/null
    chown metin2:metin2 "$STATE_DIR" 2>/dev/null

    if [ -d "$SPOOL" ]; then
        chgrp m2spool "$SPOOL" 2>/dev/null && chmod 2770 "$SPOOL" 2>/dev/null
    else
        say "no spool directory at $SPOOL -- rate changes from the panel will not arrive."
        say "  Mount the rates-spool volume there (see docker-compose.yml)."
    fi

    if [ "$LOG" != /dev/null ] && [ -f "$LOG" ]; then
        chown metin2:metin2 "$LOG" 2>/dev/null
        chmod 0640 "$LOG" 2>/dev/null
    fi
    return 0
}

# ---------------------------------------------------------------------------
#  apply -- nothing to rewrite on this engine
# ---------------------------------------------------------------------------
# The rates are six event-flag rows the panel has already put into
# player.quest; the db core reads them when it starts, which is what the
# restart the supervisor is about to do is for.  Quiet and successful when
# nothing was ever asked for, like the r40250 script.
# ---------------------------------------------------------------------------
cmd_apply() {
    [ -f "$WANTED" ] || return 0
    read_wanted
    step "Experience ${RATE_EXP}%, item drops ${RATE_DROP}%, yang ${RATE_YANG}% are event flags in the database on this engine; the cores pick them up as they start"
    write_status running "the cores are restarting with the new rates"
    remember_result ok "the new rates are live"
    return 0
}

# ---------------------------------------------------------------------------
#  poll -- has the panel asked for something new?  (exit 10 = yes)
# ---------------------------------------------------------------------------
cmd_poll() {
    [ -f "$REQUEST" ] || return 0

    _id=$(kv "$REQUEST" id)
    [ -n "$_id" ] || return 0
    _seen=""
    [ -f "$LAST_ID" ] && _seen=$(cat "$LAST_ID" 2>/dev/null)
    [ "$_id" = "$_seen" ] && return 0

    _e=$(kv "$REQUEST" exp); _d=$(kv "$REQUEST" drop); _y=$(kv "$REQUEST" yang)

    mkdir -p "$STATE_DIR" 2>/dev/null
    printf '%s\n' "$_id" > "$LAST_ID" 2>/dev/null

    for _v in "$_e" "$_d" "$_y"; do
        case "${_v:-x}" in ''|*[!0-9]*)
            RATE_EXP=$(sane_rate "$_e"); RATE_DROP=$(sane_rate "$_d"); RATE_YANG=$(sane_rate "$_y")
            fail "The panel sent a rate that is not a number -- nothing was changed"
            remember_result failed "the panel sent a value that is not a number"
            write_status failed "the panel sent a value that is not a number"
            return 0 ;;
        esac
        if [ "$_v" -lt 1 ] || [ "$_v" -gt 10000 ]; then
            fail "The panel sent a rate outside 1-10000 -- nothing was changed"
            remember_result failed "the panel sent a value outside 1-10000"
            write_status failed "the panel sent a value outside 1-10000"
            return 0
        fi
    done

    { printf 'exp=%s\ndrop=%s\nyang=%s\n' "$_e" "$_d" "$_y"
      printf 'time=%s\n' "$(date +%s 2>/dev/null)"
    } > "$WANTED.new" 2>/dev/null && mv "$WANTED.new" "$WANTED" 2>/dev/null || {
        rm -f "$WANTED.new"
        fail "Could not write $WANTED -- the state volume is not writable"
        remember_result failed "the game container could not store the new rates"
        write_status failed "the game container could not store the new rates"
        return 0
    }

    RATE_EXP="$_e"; RATE_DROP="$_d"; RATE_YANG="$_y"
    note "the panel asked for experience ${_e}%, item drops ${_d}%, yang ${_y}% (request $_id) -- restarting the cores"
    return 10
}

# ---------------------------------------------------------------------------
#  publish -- write the status the panel reads
# ---------------------------------------------------------------------------
cmd_publish() {
    [ -f "$WANTED" ] || return 0
    read_wanted
    if [ $# -ge 1 ]; then
        write_status "$1" "${2:-}"
        return 0
    fi
    [ -f "$RESULT" ] || return 0
    write_status "$(kv "$RESULT" state)" "$(kv "$RESULT" message)"
    return 0
}

case "${1:-}" in
    prepare) cmd_prepare ;;
    apply)   cmd_apply ;;
    poll)    cmd_poll ;;
    publish) shift; cmd_publish "$@" ;;
    *) sed -n '2,6p' "$0" | sed 's/^# \{0,1\}//'
       echo "usage: m2-rates prepare|apply|poll|publish [state [message]]"
       exit 1 ;;
esac
