android-native: auto hunt, in-client account registration, mobile UI polish
- Auto hunt (client-only): PythonPlayerAutoHunt.cpp + AutoHuntScript.inc as the embedded mt_autohunt module, AUTO touch button, F9/F8. - Account registration: tools/40250/register_server.py (rc.d mt_register, :11080) inserts into account.account; net.RegisterAccount/GetRegisterAccountResult (PythonAccountRegister.cpp) and the mt_register login-window dialog (RegisterScript.inc). - --py-exec also runs in --login-screen mode for scripted login-screen tests. - Touch controller, window manager and render command updates for the Android native client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
53d06c3c26
commit
3e3708ef06
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PROVIDE: mt_register
|
||||
# REQUIRE: LOGIN mysql
|
||||
# KEYWORD: shutdown
|
||||
#
|
||||
# Account registration service for the 40250 server (tools/40250/register_server.py).
|
||||
# Install: /usr/local/etc/rc.d/mt_register, then `sysrc mt_register_enable=YES`.
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name=mt_register
|
||||
rcvar=mt_register_enable
|
||||
load_rc_config $name
|
||||
|
||||
: ${mt_register_enable:=NO}
|
||||
: ${mt_register_dir:=/usr/metin2/server/register}
|
||||
|
||||
pidfile=/var/run/${name}.pid
|
||||
procname=/usr/local/bin/python3.9
|
||||
command=/usr/sbin/daemon
|
||||
command_args="-f -p ${pidfile} -o ${mt_register_dir}/register.log ${procname} ${mt_register_dir}/register_server.py"
|
||||
|
||||
run_rc_command "$1"
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/local/bin/python3.9
|
||||
"""Account registration service for the 40250 server (PORT: 40250 has no in-client registration).
|
||||
|
||||
POST /register with a form body `login=..&password=..&social_id=..` inserts a row into
|
||||
account.account the same way the existing accounts are stored (password = MySQL PASSWORD()).
|
||||
The reply is one plain-text line the client shows directly: `OK` or `ERR <CODE>`, where CODE is
|
||||
BAD_LOGIN, BAD_PASSWORD, BAD_SOCIAL_ID, EXISTS, RATE_LIMIT, BAD_REQUEST or SERVER.
|
||||
|
||||
Deployed to /usr/metin2/server/register/ and started by /usr/local/etc/rc.d/mt_register.
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
LOGIN_RE = re.compile(r"^[A-Za-z0-9]{4,16}$") # LoginWindow ID_EditLine input_limit 16
|
||||
PASSWORD_RE = re.compile(r"^[\x21-\x7e]{4,16}$") # printable ASCII, Password_EditLine input_limit 16
|
||||
SOCIAL_ID_RE = re.compile(r"^[0-9]{7}$") # the delete code introselect asks for
|
||||
|
||||
RATE_WINDOW = 3600
|
||||
RATE_MAX = 5 # successful registrations per IP per hour
|
||||
MAX_BODY = 1024
|
||||
|
||||
_rate_lock = threading.Lock()
|
||||
_rate = {} # ip -> [timestamps of successful registrations]
|
||||
|
||||
|
||||
def log(msg):
|
||||
sys.stdout.write(time.strftime("%Y-%m-%d %H:%M:%S ") + msg + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def rate_limited(ip):
|
||||
now = time.time()
|
||||
with _rate_lock:
|
||||
hits = [t for t in _rate.get(ip, []) if now - t < RATE_WINDOW]
|
||||
_rate[ip] = hits
|
||||
return len(hits) >= RATE_MAX
|
||||
|
||||
|
||||
def rate_record(ip):
|
||||
with _rate_lock:
|
||||
_rate.setdefault(ip, []).append(time.time())
|
||||
|
||||
|
||||
def create_account(login, password, social_id):
|
||||
# Every value is validated above; the password additionally travels as hex so no quoting
|
||||
# can leak into the statement.
|
||||
sql = ("INSERT INTO account.account (login, password, social_id, status, create_time, last_play) "
|
||||
"VALUES ('%s', PASSWORD(UNHEX('%s')), '%s', 'OK', NOW(), NOW())"
|
||||
% (login, password.encode("ascii").hex(), social_id))
|
||||
r = subprocess.run(["/usr/local/bin/mysql", "-uroot", "-N", "-e", sql], capture_output=True, text=True, timeout=10)
|
||||
if r.returncode == 0:
|
||||
return "OK"
|
||||
if "Duplicate entry" in r.stderr or "ERROR 1062" in r.stderr:
|
||||
return "ERR EXISTS"
|
||||
log("mysql error: " + r.stderr.strip())
|
||||
return "ERR SERVER"
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "mt-register/1"
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
pass
|
||||
|
||||
def reply(self, code, text):
|
||||
body = (text + "\n").encode("ascii")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/ping":
|
||||
self.reply(200, "OK")
|
||||
else:
|
||||
self.reply(404, "ERR BAD_REQUEST")
|
||||
|
||||
def do_POST(self):
|
||||
ip = self.client_address[0]
|
||||
if self.path != "/register":
|
||||
return self.reply(404, "ERR BAD_REQUEST")
|
||||
try:
|
||||
n = int(self.headers.get("Content-Length", "0"))
|
||||
except ValueError:
|
||||
n = -1
|
||||
if n < 0 or n > MAX_BODY:
|
||||
return self.reply(400, "ERR BAD_REQUEST")
|
||||
try:
|
||||
form = parse_qs(self.rfile.read(n).decode("ascii"), keep_blank_values=True)
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
return self.reply(400, "ERR BAD_REQUEST")
|
||||
login = form.get("login", [""])[0]
|
||||
password = form.get("password", [""])[0]
|
||||
social_id = form.get("social_id", [""])[0]
|
||||
if not LOGIN_RE.match(login):
|
||||
result = "ERR BAD_LOGIN"
|
||||
elif not PASSWORD_RE.match(password):
|
||||
result = "ERR BAD_PASSWORD"
|
||||
elif not SOCIAL_ID_RE.match(social_id):
|
||||
result = "ERR BAD_SOCIAL_ID"
|
||||
elif rate_limited(ip):
|
||||
result = "ERR RATE_LIMIT"
|
||||
else:
|
||||
result = create_account(login.lower(), password, social_id)
|
||||
if result == "OK":
|
||||
rate_record(ip)
|
||||
log("%s register %s -> %s" % (ip, login, result))
|
||||
self.reply(200, result)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bind", default="0.0.0.0")
|
||||
ap.add_argument("--port", type=int, default=11080)
|
||||
args = ap.parse_args()
|
||||
httpd = ThreadingHTTPServer((args.bind, args.port), Handler)
|
||||
log("listening on %s:%d" % (args.bind, args.port))
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user