ReoservThe rust powered Endless Online server emulator

REOSERV is finally released!

After many months of work I'm happy to announce the first official release of REOSERV.

You may have noticed the new panel on the site for the latest release. This is pulling from the latest release on GitHub!

I started using cargo-dist recently to automatically build and release reoserv any time a new tag is pushed. It's a great tool for rust projects. I highly recommend it.

There is one issue right now with windows releases that the cargo dist team is working to fix. But for now if you want to download the windows release you should also download either the Mac or Linux release as well and extract everything but the binary from that release and then only extract the Windows executable from the windows release. It's a pain but hopefully this gets fixed soon.

What's new?

I've been super busy the last few months implementing the remaining features into REOSERV. These are all done now!

  • Kill steal protection system
  • Bard skill (Instruments)
  • Guild system
  • PK Zones (PvP)
  • Anti-speed system
  • Boss & Minion system
  • Law & Marriage
  • Quest system
  • Map mutation
  • Map evacuation
  • "Deep" client compatibility

There's also been many, many, many, many bug fixes and performance improvements:

REOSERV got some new contributors!

There's finally more people than myself working on this project and I couldn't be happier!

  • Abhinandan Panigrahi (@chrzrdx) - Has done a TON of work on the website improving styles, fonts, basically everything. Doesn't it look great now?
  • Andrei Popescu (@0xDaybreak) - Fixed a windows build issue for eolib-rs and implemented group healing in REOSERV

Also need to mention @gonebaby from the EO Dev discord who created a challenge for people to develop a quest using reoserv!

Connection security

REOSERV can now be configured to:

  • Limit how many connections are allowed from a single IP address
  • How often an IP must wait before opening a new connection
  • The maximum amount of "uninitialized" connections

Simplified packet sending code

Rather than doing all this manual packet serialization all over the place I've added some helper methods that do it instead. The result is much cleaner!

Before

let reply = BankReplyServerPacket {
	gold_inventory: character.get_item_amount(1),
	gold_bank: character.gold_bank,
};

let mut writer = EoWriter::new();
if let Err(e) = reply.serialize(&mut writer) {
	error!("Failed to serialize BankReplyServerPacket: {}", e);
	return;
}

character.player.as_ref().unwrap().send(
	PacketAction::Reply,
	PacketFamily::Bank,
	writer.to_byte_array(),
);

After

character.player.as_ref().unwrap().send(
	PacketAction::Reply,
	PacketFamily::Bank,
	&BankReplyServerPacket {
		gold_inventory: character.get_item_amount(1),
		gold_bank: character.gold_bank,
	},
);

Packet rate limiting

You can now configure how often a client packet should be accepted by the server. This is useful to prevent people from spamming your server with large/heavy requests and speeding.

See the docs for more.

Packet handling moved to player thread

This one is pretty wild. When I first started working on the async version of REOSERV I had zero experience writing async rust. Way back in Feb 2022 I wrote a blog about me learning the actor pattern.

This was great because I was finally able to start making some progress. But as we all know over time you learn new things. You re-visit old code and have no idea what you were thinking.

Well recently I was just looking over the whole Packet handling system (see below) and I though "Huh.. why am I spawning a thread for this?"

if let Some(packet) = player.queue.get_mut().pop_front() {
	player.busy = true;
	tokio::spawn(handle_packet(
		packet,
		player_handle.clone(),
		player.world.clone(),
	));
}

At this point in execution I already have access to the player data. Every single packet handler needs to do something with the player.

The way I've been doing it for 2 years has basically just added an extra layer of complexity that is completely not needed!

So that same block of code now reads:

if let Some(packet) = player.queue.get_mut().pop_front() {
    player.handle_packet(packet).await;
}

This may not seem like a big deal but in my imagination (since I don't actually have performance metrics) this is huge.

For example let's look at a simple handler for a player sending an Emote.

Old

async fn report(reader: EoReader, player: PlayerHandle) {
    let player_id = match player.get_player_id().await {
        Ok(id) => id,
        Err(e) => {
            error!("Error getting player id {}", e);
            return;
        }
    };

    let report = match EmoteReportClientPacket::deserialize(&reader) {
        Ok(report) => report,
        Err(e) => {
            error!("Error deserializing EmoteReportClientPacket {}", e);
            return;
        }
    };

    if let Ok(map) = player.get_map().await {
        map.emote(player_id, report.emote);
    }
}

New

fn emote_report(&mut self, reader: EoReader) {
	if let Some(map) = &self.map {
		let report = match EmoteReportClientPacket::deserialize(&reader) {
			Ok(report) => report,
			Err(e) => {
				error!("Error deserializing EmoteReportClientPacket {}", e);
				return;
			}
		};

		map.emote(self.id, report.emote);
	}
}

The most obvious different is the new function is not async. This means the task won't be waiting for anything. The next biggest difference is that the function is now a method of the Player struct itself. This means we already have access to all of that data.

We no longer need to wait for player_id and map to come back from a different thread. It's already here. We simply use it.

This is a really simple example but belive me in some of the other handlers this has cut down on a TON of pointless back and forth between the player thread and the now non-existent "Packet Handler" thread.

Hopefully that was clear :)

Much more work moved to the player thread

This improvement is all about keeping the World and Map actors from getting stuck waiting as much as possible.

Consider this code from the map actor when a player requests to view a board post

pub async fn view_board_post(&self, player_id: i32, post_id: i32) {
	// snip
	let board_id = match player.get_board_id().await {
		Some(board_id) => board_id,
		None => return,
	};
	// snip
}

What we're saying here is "Wait until I get back the board id from the Player that requested this post". During this waiting time the map can do nothing else. Now it hasn't really been an issue because computers are fast and the time this takes is negligible but it still bothers me.

So what do we do to get rid of this lag? Well we change the method so that the board id is passed along from the player to begin with.

pub fn view_board_post(&self, player_id: i32, board_id: i32, post_id: i32) {
    // do something with board_id
}

This is great! No more waiting on something from the player. The function doesn't need to be async anymore either!

This was a pretty simple example but this kind of stuff was (and still is) all over the Map and World actors. I've cleaned up a lot of it but there's still more to go.

The goal I'm striving for is to move as much work as possible to the Player actor so that any slow down just affects that Player and not the entire Map or World

Thanks for reading

I'm not done with REOSERV. There's still so much I want to do like finish refactoring extra work done by the Map and World actors, implement tracing for performance monitoring, create a web control panel for monitoring and interacting with the server.

See you in the next update.