Project Echo - Fully Modular V14+ Server [Java, SQLite, ORM]

Developer
Developer
Joined
Dec 11, 2010
Messages
3,025
Reaction score
2,790
Echo Project
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:

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)
Don't like SQLite? Just edit Echo-Storage to point to a different SQL driver :')



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 ( ) - handles encryption and login
  • Echo-Module-Navigator ( ) - room listing, categories, navigation logic
  • Echo-Module-Room ( ) - room instances, entity movement, interactions
  • Echo-Storage ( ) - abstracted persistence using an ORM, can switch from SQLite to MySQL and so-forth
  • Echo-API ( ) - 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 :')

Quackster - Project Echo - Fully Modular V14+ Server [Java, SQLite, ORM] - RaGEZONE Forums


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:
Thank you everybody!

A few important notes:

- Everything is subject to change (the original thread may not get updated, please check the or see the latest posts in the dev thread)
- Yes, I know there's anti-patterns in some areas, the API is still under development!
- "Project Echo" is actually just a temporary/placeholder name, but I might keep it.
- Class names are subject to be renamed, code is subject to be refactored, as the API gets more developed.
- I will likely move to v21+ once I get the encryption working for it.
- There will be documentation on how to write plugins.
- Any class can have event handler methods despite it not implementing/extending any listeners.

About the libraries I'm using:

I'm using a few libraries I've written myself:

- I've written my own ORM for this called because I wasn't satisfied with other libraries. This translates Java code to queries to run for the specified RDBM system, everything is parametrised.
- I've written my own logging framework called which is being used.
- I've written my own dependency injection framework called which is being used.

Awesome job. Super cool. I wondered why no one ever did this before. I started something similar a couple years back but got bored.

We were all a bunch of silly teenagers that cared about getting features out as fast as possible without any concerns of using antipatterns. :)
 
Last edited:
Thank you everybody!

A few important notes:

- Everything is subject to change (the original thread may not get updated, please check the or see the latest posts in the dev thread)
- "Project Echo" is actually just a temporary/placeholder name, but I might keep it.
- Class names are subject to be renamed as the API gets more developed.
- I will likely move to v21+ once I get the encryption working for it.
- There will be documentation on how to write plugins.
- Any class can have event handler methods despite it not implementing/extending any listeners

About the libraries I'm using:

I'm using a few libraries I've written myself:

- I've written my own ORM for this called because I wasn't satisfied with other libraries. This translates Java code to queries to run for the specified RDBM system, everything is parametrised.
- I've written my own logging framework called which is being used.
- I've written my own dependency injection framework called which is being used.



We were all a bunch of silly teenagers that cared about getting features out as fast as possible without any concerns of using antipatterns. :)
Truth, it was a lot cooler to be the first to get a feature than it was to actually test something and ensure that it works properly in all cases, lol.
 
Back