Removes now-unreachable #ifdef _WIN32 branches across Common/ and the three
server binaries (all builds are Linux-only), deletes the entirely unused
SEH exception-handling shim and an orphaned vendored MySQL client header.
Enables LTO and -march=x86-64-v2 in Release builds for a small free perf win.
Also seeds a default GM account (gmadmin/changeme) via
database sql files/gm_account.sql so a fresh deploy has something to log
into the web admin panel with, switches the MariaDB data directory from a
Docker named volume to a host bind mount for visibility/backup, and expands
the README with a fuller Docker deployment walkthrough.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CharServer/, Login/, WorldServer/, and Servers/ contained nothing but Visual
Studio project files -- no source, nothing else. CMake/Docker is the only
build path now, so these were pure dead weight.
Covers quick start, the two web UIs, non-Docker build instructions, repo
layout, and the two Windows-porting gotchas (clock() semantics, DB string
widths) most likely to bite future changes.
Map names now come from LIST_ZONE.STB (row index = map id, matching
CWorldServer::LoadZoneData). New NPCs tab lists all placed NPCs from
list_npcs with coordinates; new Monsters tab lists all 1,193 named montypes
from LIST_NPC.STB. Both are click-to-copy GM commands (/tele, /mon) for
quick GM navigation/spawning.
Previously stomped the shared GServer->MOVE_SPEED_MOD movement-timing
divisor, which character.cpp's UpdatePosition also uses for monster position
interpolation -- GM testing broke movement/attack range server-wide until
restart. Now stores a per-character SpeedModPercent that GetMoveSpeed()
applies on every recompute, so it survives future SetStats() calls instead
of being silently overwritten back to base speed. Also fixes Mspd_base (the
value actually broadcast to clients) never being set while WALKING.
Docker SDK drives login/char/worldserver containers via docker.sock (mounted
read-only into webadmin); live logs stream over SSE with nginx buffering
disabled so lines arrive in real time. Broadcast/kick go through a new
admin_queue DB table that the world server polls every ~2s in its existing
WorldProcess tick loop, avoiding a second IPC channel to the game server.
Removes Visual Studio local-state files (.vs/, .vcxproj.user, .suo/.VC.db),
precompiled Windows server binaries (now built from source via Docker), and
"Copy of"/.bak duplicate source/data files that were never referenced by the
CMake build. Adds matching .gitignore rules so they don't come back.
webtools' nginx now reverse-proxies /admin/ to the webadmin container
(docker/conf/webtools-nginx.conf), which no longer publishes its own
port -- everything is reachable through port 80 alongside the GM
toolbox. Added a link to it in webtools/index.html's sidebar.
Flask needs to know it's mounted under /admin/ to generate correct
url_for()/redirect() targets, so app.py now wraps the app in
werkzeug's ProxyFix (x_prefix/x_proto/x_for/x_host) reading the
X-Forwarded-Prefix header nginx sets; verified redirects, form
actions, and links all come back /admin/-prefixed end to end through
port 80.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CCharacter::UseAtkSkill's hitmod formula was missing the "+ Stats->Level"
term that CCharacter::NormalAttack's equivalent hitmod already has --
despite the comment directly above it ("skill attacks now based on a
percentage of base attack power so that they scale with level")
promising exactly that. Net effect: a skill with atkpower=100 computed
the same base Attack as a plain auto-attack, but without the level
bonus or crit chance auto-attacks get, so skills ended up hitting no
harder (often weaker) than basic attacks.
This only touches genuine damage skills (skilltype 3/6 dispatched via
SKILL_ATTACK in worldpackets.cpp:pakStartSkill); buffs/heals/status
effects/resurrect (skilltype 9/11/13/20, SKILL_BUFF and friends) run
through entirely separate code and are untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renaming checks for a name collision first (characters.char_name has
no unique constraint in the schema, so this has to be enforced at the
application level to avoid two characters silently sharing a name),
and also updates list_friend.namefriend for any rows referencing this
character's id -- that column is a denormalized display-name cache in
other players' friend lists and would otherwise go stale on rename.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Traced how login placement actually works: CPlayer login placement
(Src/World Server/worldpackets.cpp:27, pakPlayer) calls
GetRespawnByID(Position->respawn), and Position->respawn is loaded
straight from characters.respawnid (playerdata.cpp:110). So setting
respawnid to a Src/World Server list_respawnzones.id is a real, live
lever over where a character logs in next -- not a vestigial column
(unlike characters.townid, which turned out to be dead: the only
other code path that reads it is commented-out dead code in
pakUserDied, superseded by CPlayer::RespawnPlayer which uses an
in-memory Position->saved instead).
Added to the character edit page: a teleport dropdown grouped by map,
populated from list_respawnzones, wired straight to respawnid (takes
effect on the character's next login same as other stat edits) and a
delete-character action. Added to the account edit page: delete
account (characters are kept) and a "reset online flag" action for
accounts stuck thinking they're still logged in after a crash.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New "webadmin" service (Flask + PyMySQL, port 8081) backing a small
server-rendered admin UI:
- Login gated on an existing game account: only accounts with
accesslevel >= ADMIN_MIN_ACCESSLEVEL (default 900) get in. Password
is MD5-hashed before comparison since the game client hashes it
client-side before it ever hits the wire -- accounts.password is
always an MD5 hex digest, confirmed against existing DB rows.
- Accounts: list/search, create, edit accesslevel/active/password.
- Characters: list/search, create (mirrors the real char-creation
INSERT in Src/Char Server/charpackets.cpp -- only account_name,
char_name, face, hairStyle, sex are set, everything else relies on
the table's own defaults), edit core stats (level, zuly, attributes,
hp/mp, stat/skill points) and isgm/isdev flags directly.
All queries are parameterized (no string-formatted SQL). No CSRF
protection or rate limiting yet -- acceptable for a small
already-behind-a-high-accesslevel-gate private server, worth
revisiting before ever exposing this port publicly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CWorldServer::RandNumber (and three inlined copies of the same pattern
in worldpackets.cpp's item-refine code) computed "range*rand()" in
integer arithmetic before dividing by (RAND_MAX + 1.0). On Windows
RAND_MAX is 32767 (15-bit), so this never overflowed. On Linux/glibc
RAND_MAX is 2147483647 (2^31-1), so "range*rand()" overflows a 32-bit
UINT for the vast majority of rand() outputs, wrapping to a small
value -- the division by ~2^31 then collapses the result to ~0 or 1
almost always, regardless of the requested range. This made every
percentage-based roll in the game (item drop selection, refine
durability/stats, and anything else using RandNumber) behave as if
called with range 1-2 instead of the real range, which is why item
drops effectively never triggered post-port. Cast to double before
the multiply so it can't overflow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Welcome message default/example changed from "Welcome to ROSE" to
"Welcome to ROSS", and the hardcoded post-login system message
replaced with a ROSS-specific credit line.
Also re-enable the codedebug SendPM lines in GetNewDrop that report
the raw randv/money/zulyrate/item/itemrate values behind each
zuly-vs-item roll (previously commented out) -- needed to diagnose
in-game why item drops are still rare/absent after the drop-rate and
item-index fixes, without guessing at STB drop-table values blind.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetNewDrop's weighted item-index selection used a do-while that
increments before its first RunningTotal check, so index 0 (the first
item slot in every drop table) could never be selected -- for the 51
drop tables that only populate item slot 1 (all others zero), this
silently discarded every roll ("item at itemindex does not exist"),
so those mobs/maps never dropped an item at all. Switch to a pre-check
while loop so index 0 is reachable.
Also: battle.cpp's kill-drop loop runs GetDrop()/AddDrop() exactly
Stats->itemdroprate + Config.DROP_RATE times, not as a probability --
with the shipped drop_rate=2 every kill produced two independently
rolled drops (hence always-doubled Zuly boxes for characters with no
item-drop-rate gear). Default drop_rate to 1 so a kill yields one
roll by default; gear/GM commands that raise itemdroprate still grant
extra rolls as designed.
Also default dropdelay to 0: the killed-monster's loot became visible
to clients only after Config.DropDelay (1s) *plus* the next
VisualDelay tick (200ms), giving a ~1.2s lag before drops appeared.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Client/, 3Ddata*, KTRose_pre-evo.zip are large local-only assets used
for end-to-end login/game testing, not part of the server source tree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds an nginx:alpine service serving webtools/ (item/map lookup
frontend) on port 80 alongside the existing login/char/world stack.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
accounts.lastlogin/logtime had no defaults and were NOT NULL, but the
auto-create-account INSERT never set them, so new accounts failed
under MariaDB strict mode (silently accepted under the old MySQL
5.0 target). Insert explicit empty/zero values and give lastlogin a
default; also widen it to varchar(32) to match the runtime.sql fix
already applied to the starttime column.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetNewDrop() allocated newdrop up front but returned NULL without
freeing it on every rejection path (failed drop-rate check, missing
drop table, unknown item type, etc.), leaking on every non-drop roll.
Free it on all early-return paths, and only assign a clientid once a
drop is actually going to be returned.
CPacket::AddByte/Word/DWord/QWord/Float now bounds-check against the
buffer before writing, preventing an overflow if a packet is built
past its allocated size.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Several quest-loading loops used <=N over arrays sized N (Variables,
Switches, Items, EpisodeVar, JobVar, PlanetVar, UnionVar), writing one
element past the end on every character load. Also guard STB_QUEST
table lookups with a rowcount bounds check before indexing by
QuestID, which could otherwise read out of bounds for stale/invalid
quest ids.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POSIX clock() sums CPU time across every thread in the process, unlike
Windows' clock() which tracks wall-clock milliseconds. With several
worker threads running, respawn/spawn/AI timers based on clock() raced
far ahead of real elapsed time. Switch spawn-timer bookkeeping to
time(NULL)/time_t (seconds) throughout the world server, and add a
portable clock() shim in sockets.h (backed by CLOCK_MONOTONIC,
millisecond resolution) for remaining call sites so CLOCKS_PER_SEC==1000
still holds like it did on Windows.
Also: fix MapDelay/VisualDelay/WorldDelay being passed to usleep() (us)
without converting from their configured milliseconds; fix the shutdown
timer likewise; stop leaking the temporary actor used to run an NPC's
AIP script; fix CleanDrops() skipping the element that shifts into
position after an erase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>