4 online Record 53 Last Man Standing Sat 20:00 Server etheraclassic.com Discord Vote on OTServList
EtheraClassic 8 · Tibia & Pokémon Play free

Behind the scenes

Dev notes

How Ethera is built, and why it was built that way. The What's new page says what changed; this one says how it works and what went wrong on the way.

7 October 2026

Server / Events

Raids, and why the engine's own timer could not be used

Raids were switched off on this server, and simply switching them on would have been a mistake. The engine's raid timer fires on a fixed probability and knows nothing about who is online, which goes wrong three ways at once: Ferumbras appears at four in the morning and wanders unkilled until the next restart; the Piratess raid drops 524 monsters whether two people are online or two hundred; and nothing ever clears up what was not killed.

So the engine timer stays off - which still loads the raid files - and a raid director owns the scheduling instead. It reads the raid files once at startup and decides three things.

Whether the people online can fight it. The bar comes from the raid's own worst monster, so no raid had to be tagged by hand, and it is a level bar rather than a headcount: four level-8 characters are not a boss raid. Getting those floors wrong is easy - the first version let the lowest band through at level 1, and a single level-20 character counted as enough for forty of the sixty-one raids. With the floors corrected the same character qualifies for twenty-six, every one of them genuinely low-level, and the four world-boss raids need four players at level 180 or more.

How big it should be. The amounts in the raid files are now a busy-server maximum; what actually spawns is about eight creatures per online player, capped. Two players get a skirmish of eight, twenty get the full thing. This is also the cheapest performance win available - a 524-monster raid was a lag source by itself - and monsters are created a few at a time so even a large raid never stutters.

When it is over. Survivors are removed once the raid has run its course, and immediately if the last player logs out. A boss can no longer outlive the players who were meant to fight it.

Every raid is now the same shape of event: one announcement naming the place and the level it suits, three minutes to travel there, then the creatures land, and a line when the attack is beaten off. That applies to the forty-nine raids that were already here as well as the new ones, because a raid's location is worked out from its own coordinates rather than written down anywhere.

Twelve new raids were added, deliberately clustered on the core continent - three around Thais, two on the Plains of Havoc road, two at Carlin, two at Venore, two at Ab'Dendriel and one in the Kazordoon mines - built from weak creatures so a new character has events worth attending. Their spawn areas were picked from places where weak monsters already live, so the ground is known to be walkable and the area reachable on foot. The whole catalogue is weighted towards those towns too.

One knock-on: raids running again means the ten raid-only monsters become catchable, so the Pokedex went from 235 species to 242. The three world bosses are deliberately left out - they appear only when the director finds four high-level players online, and gating the last Pokedex rank behind being awake for one of those would make "catch them all" luck rather than effort. That is one line to change if it should count.

Staff can call an event by hand with /raid, list what suits the current crowd, or stop one; anyone can type !raid to see what is happening.

Server / Pokedex

Can the Pokedex actually be finished?

A question worth asking of any "catch them all" system: can it actually be finished? Three things were checked, and two of them were broken.

The roster is honest. It is not a hand-written list - it is built from what a player can actually meet: every species in the spawn file, every species a raid spawns (only while raids are switched on), and anything those summon. That is why it cannot list something unobtainable. It now holds 235 species, 233 of which have a spawn on the map. The other two are reachable but not by walking to them: Gazer is only ever summoned, by an Elder Beholder or an Elder Bonelord, and Fernfang only appears as the Sunny Meadow boss.

Fernfang was missing. The Hunters' Plaza grounds create their boss from a script rather than from the spawn file, so the roster never knew about it: Fernfang could be hunted and killed, counted for nothing and never appeared in the list at all. The roster now reads the island table too. The same check cleared the arena fighters and the league rivals, which are scripted opponents rather than wildlife - they were already falling out of the roster for want of a spawn, and that is now an explicit rule instead of an accident.

"Seen" never worked. The Pokedex has three states - unknown, seen, caught - and the middle one was dead. An extended-opcode event only fires for players it is registered on, and the bestiary's was never registered at login, so the client's "I can see a Dragon" message was thrown away every time. Species could only jump from unknown straight to caught. One line, and the minimap's seen markers work as designed. The server's check that the creature really is on screen is unaffected: a spoofed sighting of something far away is still refused.

Every rank is reachable. The ranks are absolute counts and the hardest below the last needs 200 species, comfortably under 235. Worth re-checking if the roster ever shrinks - at 199 species, Grand Champion would quietly become impossible while Pokedex Master stayed earnable.

Bosses are a different story. Raids are switched off in the configuration, which means ten monsters named only in raid files cannot appear at all: Ferumbras, Ghazbaran, Morgaroth, Orshabaal's raid copy, Fernfang's raid copy, Deadeye Devious, Lethal Lissy, Brutus Bloodbeard, Munster and the Eldenbloods. The roster correctly leaves them out, so nothing is broken - but it is worth knowing that switching raids on would add them to "catch them all", and three of those are world bosses that appear perhaps once in a server's lifetime. Turning raids on without rethinking the last rank would make a full Pokedex a matter of luck rather than effort.

Twenty-four species have only one or two spawns in the whole world. Most are the Ankrahmun tomb bosses, which sit behind their own quests rather than behind a long walk. Whether every one of those rooms can still be opened has not been machine-checked - that needs a reachability pass over the map, which is a separate job.

Client / Audio

Audio removed, and what was kept from it

The sound is gone. Music, ambience and event sounds have all been removed from the client, along with the Audio options tab and the mute button, and audio is switched off at the engine so nothing can make a noise even if some module still asks.

It was built, it worked, and on reflection it was not wanted - a Tibia 7 client is a quiet thing, and every sound added was one more thing to turn off. The honest reason to write this down is that building it taught us where the map's own structure lives, and that part was kept: the tool that finds every city by clustering protection-zone tiles and matches them against the map's town table still exists, and so does the ground-material classification that covers 99.72% of tiles. If a region system is ever wanted for something other than sound - a zone name on screen, a hunting guide, experience bonuses by area - the hard half is already done.

The client download is about 2 MB smaller. The Character Records window, which arrived in the same release, is unaffected; the level and death hooks that used to ring a bell now only feed it.

Nothing was thrown away. The modules, the generated audio and the three synthesis tools are kept in the server's backup folder, so the whole thing is one copy away from coming back.

Server / Operations

One bad column stops a character saving entirely

While testing the new records, a hand-built test character stopped persisting anything at all - no records, no level, no items. The window showed the right numbers while it was online and had forgotten them by the next login.

The cause was one line in the server log: Out of range value for column 'manaspent'. The character had been given a level and a magic level that did not match its stored mana spent, and when it died the engine recomputed a value the column could not hold.

The part worth knowing is what that does. A player's save is one single UPDATE. If any column in it is rejected, the whole statement fails, and with it the level, the experience, the position, the equipment and every storage value that character holds. Nothing is partially written. The player sees no error - they simply find themselves back where they were at the last successful save.

In practice this only happens to characters created by hand rather than through the game, which is why no real player has been affected. But it is worth watching for, because the only outward sign is Error while saving player in the server log, and a character in that state quietly loses progress until someone notices.

Client / Audio

Ambience is baked out of the map, not hand-listed

The client knows where you are without the server telling it. A tool, tools/bake_ambience.py, reads all 9,663,112 tiles of global.otbm once and produces a small lookup table that ships inside the client.

Cities are found, not typed in. Every protection-zone and house tile on every floor is projected onto an 8x8 grid, dilated by 16 tiles so the gaps between buildings close into one shape, and the resulting blobs are matched against the map's own town table. That recovers all 14 towns by name. Raw protection-zone tiles alone fragment into 521 pieces, because the zone is painted per building - the dilation step is the whole trick.

Harbours come from the boat captains. A harbour is simply where a captain stands, and the spawn file already knows all eleven of them with exact coordinates.

Terrain is read live instead of baked. The client can already see the ground item ids of the tiles around you, so the biome is decided from them at runtime - a small window of tiles votes, so one odd doorstep in a meadow does not change the weather. Of the 82 distinct ground names on the map, the table classifies 99.72% of all ground tiles. Because nothing is baked per tile, editing the map changes the ambience with no rebuild.

What did not work: clustering monster spawns to find dungeons. The Tibia underground is continuously populated, so a 40-tile connected clustering of all 14,395 spawn blocks collapses into a single blob of 32,125 monsters spanning floors 0 to 15. There is no topological seam between one cave and the next, which is why named dungeons have to be written by hand rather than discovered.

Audio

Every sound here is synthesised, not downloaded

No sample packs. Three tools - make_music.py, make_sounds.py and make_ambience.py - write every piece of audio from scratch in plain Python, which keeps the whole soundtrack auditable and the licensing simple.

The palette is deliberately narrow so it all sounds like one world: struck bronze (bells modelled with the real inharmonic partial ratios 1, 2.76, 5.40, 8.93), a war horn (detuned saws through a one-pole lowpass), an anvil, and a frame drum. The ambient beds are filtered noise with slow gust envelopes, plus short voices for birds, gulls, cave drips and fire crackle.

Each bed loops seamlessly because it is built one and a half seconds longer than needed and the tail is folded back over the head. Measuring the head and tail energy of the finished files confirms the join - the cave bed reads 0.022 against 0.020.

Two problems were caught by measuring rather than listening. The beds came out varying fourfold in loudness, which would have made walking down a staircase a jump in volume, so each one now has a deliberately chosen level with the cave as the quiet one. And the lava bed was clipping after Vorbis encoding at a peak of 1.00 even though it went in under the ceiling - lossy encoding can overshoot its input. The ceiling dropped to 0.86 and the files now measure zero clipped samples.

Audio

The per-hit sound is gone, the record stayed

Landing a personal best used to sound a war horn. In testing that read beautifully; in an actual hunt it fired constantly, because early on almost every hit is a new best. A sound that happens every few seconds is not a moment, it is a nuisance.

So the horn, the announcement and the whole per-hit code path came out - including the hook that was registered on every monster you swung at. What stayed is the record itself, now kept silently and shown in the Character Records window instead. The interesting part was never the noise.

Server / Client

Character Records, and the callback the engine does not have

The window shows two kinds of number and says which is which. Records are earned once and kept in storage, so they survive a relog and a death: hardest hit, hardest melee hit, hardest shot, hardest spell, the worst blow you have survived, and the best experience per hour you have ever held. Right now figures are read off the character as the window opens - total armour worn, defence in hand, weapon attack, capacity, speed - because storing those would only be a slower way of reading your inventory.

Recording the hardest hit you deal is awkward, because the engine has no "damage dealt" callback on the attacker. The way around it is to watch from the other side: when you attack something, a health-change event is registered on that monster, and it reports the damage back to you. Re-registering the same event on a creature is a no-op in the engine, so it costs one lookup per swing.

Experience per hour is measured over a rolling fifteen-minute window and only counts once it holds five minutes of data, so a lucky spawn cannot set a record.

Verified on the live server after the restart: the equipment sums came back exactly as predicted from the item data (armour 25, defence 33, attack 14 for a known set), and a test character standing unarmoured in a rat spawn recorded its hardest hit taken within seconds.

Economy

Rune prices follow the official list

42 price entries across the rune and potion merchants were brought in line with the published price list, in both directions. Some had drifted far enough to distort the economy: sudden death from 325 to 162, explosion from 250 to 31, heavy magic missile from 125 to 12, great fireball from 180 to 64, and the necrotic and terra rods upward from 1000 and 2000 to 5000 and 10000.

Backpack prices follow the same convention the shop module uses - one backpack of twenty runes at the charge count each, so a backpack of sudden death is twenty times its unit price rather than a separately invented number.

Client

A layout trap that cost a release

The first build of the Character Records window flooded the client log with ERROR: ATTENTION the event list is not getting empty - 118 times in a sixty second run. The client did not crash; it just burned frames forever.

The cause: a panel anchored on both its top and bottom edges while also carrying fit-children: true. It resizes to fit its children, which changes its anchored height, which resizes it again. fit-children belongs only on inner containers with no competing anchors.

What makes this worth writing down is that there was already a comment warning about it in the hunt analyzer's own layout file, from the last time it happened - and it still caught the next window built. The reliable way to find it is to run the built client headless and diff the warning count against the previous release: 0 against 118 located it in one step. The released build is back to 0.

Client

Default hotkeys, outfit presets, and a spell list that is true

Hotkeys now come set up per vocation on a new character and can be changed freely afterwards, so nobody has to build a bar from nothing before their first hunt.

Outfit presets were added to Customize Character: the national colours, the legendary war outfits, and a few less serious ones. They are built against the client's own 133-colour outfit palette. Two presets had to be redone after rendering them - the client multiplies the artwork by the chosen colour, so anything near black erases the shading and leaves a silhouette. Those are mid greys now.

The spell list was showing 141 spells when the server only has 79, with wrong words and wrong mana costs on many of them. It is now generated from the server's own spell file, so it cannot drift again.

!commands reads the talkaction file at first use and opens as a book, grouped into sections, with search. It also found a real bug: writing words="!commands;!help" looks like it registers two commands, but the engine takes the whole attribute as one, so !commands silently did nothing. Spells split on the semicolon; talkactions do not.

Tooling

How a release reaches you

There are four copies of the client on this machine - the working tree, the download zip on the website, a local dev share and an admin build - and for a while they drifted apart, which is how a ten-month-old module once ended up in testing. One script, tools/release_client.sh, now writes all four from the same build and keeps the dev share's own configuration file.

Every release raises a build number in both the client and the server config; the server compares them fifteen seconds after login and tells anyone on an old build to download again. The downloads page reads the same number, so the date shown there is never hand-maintained.

Builds are smoke-tested headless before publishing - xvfb-run on the Linux binary, then the log is read for Lua errors and layout loops. It cannot test audio, because the Linux build has no sound support at all; that is checked on the Windows binaries.