- Joined
- Dec 11, 2010
- Messages
- 3,025
- Reaction score
- 2,790
Echo Project
Open-source and fully modular Java server
Open-source and fully modular Java server
What is Echo?
Project Echo is a fully modular Habbo Hotel server written in Java, intended to succeed and improve upon Kepler’s architecture. This is designed as a spiritual successor to Kepler, moving away from monolithic design to something much more flexible.
This server has been designed with everything being an extensible plugin, with it depending on API, much like Bukkit/Spigot.
Source Code & Updates
All source code is publicly available on GitHub:
To view the content, you need to sign in or register
That’s where most updates, commits, and new modules will appear first.
Follow the GitHub organization to keep up with the latest changes, issues, and development progress.
Key goals:
- Decoupled modules for core functionality (handshake, navigation, rooms, storage, etc.)
- Scalability and maintainability
- Ease of customising / extending features
- Event and plugin system
- Support multiple database RDBMS engines (SQLite, MySQL, Microsoft SQL, Postgres)
Architecture & Modules
How the project is being structured (list may not be always up to date, please check the official organisation repo):
- Echo-Module-Handshake (
To view the content, you need to sign in or register) - handles encryption and login
- Echo-Module-Navigator (
To view the content, you need to sign in or register) - room listing, categories, navigation logic
- Echo-Module-Room (
To view the content, you need to sign in or register) - room instances, entity movement, interactions
- Echo-Storage (
To view the content, you need to sign in or register) - abstracted persistence using an ORM, can switch from SQLite to MySQL and so-forth
- Echo-API (
To view the content, you need to sign in or register) - library used for all plugins
Because everything is modular, you can swap or extend plugins with less friction than a monolithic design.
Core plugins are considered modules, while custom 'modules' by the community are considered plugins.
Current Features
- Basic handshake flow is implemented.
- Simple room instantiation, entity join/leave routines underway.
- Navigator module is in early staging - listing public/private rooms, categories, filtering.
- Storage ORM implmentation
Pictures
Time for some eye candy :')
Code Snippets
RoomService.java
Java:
package net.h4bbo.echo.plugin.room.services;
import net.h4bbo.echo.api.services.room.IRoomService;
import net.h4bbo.echo.plugin.room.Room;
import net.h4bbo.echo.plugin.room.RoomPlugin;
import net.h4bbo.echo.storage.StorageContextFactory;
import net.h4bbo.echo.storage.models.room.RoomData;
import net.h4bbo.echo.storage.models.user.UserData;
import org.oldskooler.entity4j.Query;
import java.sql.SQLException;
import java.util.List;
import java.util.function.Function;
public class RoomService implements IRoomService {
public RoomPlugin plugin;
public RoomService(RoomPlugin plugin) {
this.plugin = plugin;
}
@Override
public List<RoomData> getRoomsByCategory(int categoryId) {
return this.getRooms(x -> x.equals(RoomData::getCategoryId, categoryId));
}
@Override
public List<RoomData> getRoomsByUserId(int userId) {
return this.getRooms(x -> x.equals(RoomData::getOwnerId, userId));
}
@Override
public List<RoomData> search(String queryString) {
return this.getRooms(x -> x
.open()
.equalsIgnoreCase(UserData.class, UserData::getName, queryString)
.or()
.like(RoomData::getName, queryString + "%")
.close()
.and()
.isNotNull(UserData.class, UserData::getName)
);
}
public List<RoomData> getRooms(Function<Query.Filters<RoomData>, Query.Filters<RoomData>> predicate) {
try (var ctx = StorageContextFactory.getStorage()) {
var query = ctx.from(RoomData.class).as("r")
.select(s -> s
.all(RoomData.class)
.col(UserData.class, UserData::getName).as("owner_name"))
.leftJoin(UserData.class, "u", on ->
on.eq(RoomData::getOwnerId, UserData::getId))
.filter(predicate)
.orderBy(RoomData::getVisitorsNow, false);
// System.out.println("Query: " + query.toSqlWithParams());
return query.toList();
} catch (SQLException e) {
this.plugin.getLogger().error("Error loading rooms: ", e);
}
return null;
}
}
HandshakePlugin.java
Java:
package net.h4bbo.echo.plugin.handshake;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelId;
import net.h4bbo.echo.api.event.EventHandler;
import net.h4bbo.echo.api.event.types.client.*;
import net.h4bbo.echo.api.network.codecs.ProtocolCodec;
import net.h4bbo.echo.api.plugin.JavaPlugin;
import net.h4bbo.echo.api.services.navigator.INavigatorService;
import net.h4bbo.echo.api.services.user.IUserService;
import net.h4bbo.echo.plugin.handshake.encryption.RC4Holder;
import net.h4bbo.echo.plugin.handshake.messages.handshake.InitCryptoMessageEvent;
import net.h4bbo.echo.plugin.handshake.services.UserService;
import net.h4bbo.echo.storage.StorageContextFactory;
import net.h4bbo.echo.storage.models.user.UserData;
import org.oldskooler.inject4j.ServiceCollection;
import java.sql.SQLException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public class HandshakePlugin extends JavaPlugin {
private final Map<ChannelId, RC4Holder> encryptionHolders
= new ConcurrentHashMap<>();
@Override
public void assignServices(ServiceCollection services) {
services.addSingleton(IUserService.class, UserService.class);
}
@Override
public void load() {
this.getEventManager().register(this, this);
}
@Override
public void unload() {
this.encryptionHolders.clear();
}
@EventHandler
public void onClientConnected(ClientConnectedEvent event) {
var rc4Holder = new RC4Holder(
event.getConnection()
);
this.encryptionHolders.putIfAbsent(
event.getConnection().getChannel().id(),
rc4Holder
);
event.getConnection().getMessageHandler().register(this, InitCryptoMessageEvent.class);
}
@EventHandler
public void onClientDisconnected(ClientDisconnectedEvent event) {
this.encryptionHolders.remove(
event.getConnection().getChannel().id()
);
}
@EventHandler
public void onConnectionReceivedDataEvent(ConnectionReceivedDataEvent event) {
if (!this.encryptionHolders.containsKey(event.getConnection().getChannel().id()))
return;
var rc4Holder = this.encryptionHolders.get(event.getConnection().getChannel().id());
if (!rc4Holder.isEncryptionReady()) {
return;
}
byte[] message = new byte[event.getBuffer().readableBytes()];
event.getBuffer().readBytes(message);
var deciphered = rc4Holder.rc4.decipher(message);
var buffer = Unpooled.buffer(deciphered.length);
buffer.writeBytes(deciphered);
event.setBuffer(buffer);
}
public Optional<UserData> getUserAuthenticate(String username, String password) {
return getServices().getRequiredService(IUserService.class)
.getUserAuthenticate(username, password);
}
public Map<ChannelId, RC4Holder> getEncryptionHolders() {
return encryptionHolders;
}
}
Last edited:

