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.
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>
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>
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>
- runtime.sql: widen starttime to varchar(32). ctime() produces 25 chars;
MariaDB's default strict mode rejects the 20-char column instead of
silently truncating like the original MySQL 5.0 target did.
- SocketServer.cpp/SocketBase.cpp: the accept-loop's select() used `maxfd`
set only from `sock`, never updated for the ISC sockets (sckISC/sckISCII).
Any fd beyond nfds is left however FD_SET last set it, since select()
never examines it -- so those bits stayed permanently "set", and the
server treated them as always-ready. For the login server specifically
this was fatal: its sckISC is created but never listen()'d (the login
server has no inbound ISC use), so accept() on it returned EINVAL on
every single loop iteration, spinning the login server's CPU and
spamming the log without ever getting to the real client-facing accept()
in a timely way. Root-caused with a real client connecting for the first
time (this path was never exercised end-to-end before). Fixed by
including the ISC sockets in maxfd and skipping the ISC accept branch
entirely for the login server. Also initialize sckISC/sckISCII to
INVALID_SOCKET in the constructor -- they were previously left
uninitialized for server types that never touch them.
- tools/rose_login_test.cpp: protocol-level login smoke test. Links
directly against Src/Common/rosecrypt.cpp (the server's own encryption
code) rather than reimplementing the crypto, so there's no risk of a
second, subtly-wrong implementation. Documents the wire handshake
(0x703 encryption request -> 0x7FF -> 0x708 login, password[32]+username[16]
payload) for future reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rebuilds (not Wine) the login/char/world servers natively for Linux via
CMake, and ships a single Docker image plus docker-compose stack (MariaDB +
3 servers) with all settings configurable via .env.
Linux portability:
- Src/Common/compat.h shims MSVC-only CRT calls (fopen_s, _strdup,
strncpy_s, sprintf_s, SYSTEMTIME/GetLocalTime, WSAGetLastError, __cdecl)
and adds a case-insensitive/backslash-tolerant path resolver for the
Windows-authored STB/AIP data file paths.
- Fixed case-sensitive header includes and filenames (charserver.h vs
CharServer.h, worldserver.h vs WorldServer.h, ../common vs ../Common).
- rose_inet_addr() resolves hostnames (not just literal IPs) for the
server-to-server links, so docker-compose service names work.
Fixed three real bugs that only crashed on 64-bit Linux (silently tolerated
on 32-bit Windows):
- CIntStb::LoadSTB had an inverted fopen success/failure check, so STB game
data (zones, items, etc.) was never actually loaded.
- GenerateCryptTables allocated its pointer table with sizeof(unsigned)
instead of sizeof(unsigned*) -- a 1x vs 2x-size heap overflow on 64-bit.
- Crypto bit-twiddling relies throughout on signed-overflow-as-wraparound,
which is UB in C++; GCC's -O3 exploited it and miscompiled
GenerateCryptTables (Release-only crash). Fixed via -fwrapv.
Plus smaller fixes: off-by-one array bounds (inventory/storage/sell data),
SO_REUSEADDR on server sockets (containers restarting in the same netns hit
TIME_WAIT otherwise), and a va_list reused across multiple vprintf/vfprintf
calls in Log() that garbled every formatted log line on the Linux ABI.
Docker:
- CMakeLists.txt builds all three servers + shared Common code.
- Dockerfile: multi-stage build, one runtime image for all three servers,
server role selected via `command: [login|char|world]`.
- docker-compose.yml: MariaDB (auto-imports database sql files/ on first
run) + login/char/world (single channel), config rendered from
docker/conf/*.conf.tmpl via envsubst at container start.
- .env.example documents every setting (ports, server names, rates,
inter-server passwords, PUBLIC_HOST for internet-facing deployments).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>