On my work on l2jserver2, which I believe is one of the best architectured server software, I solved some pretty tough design issues.
Implementation independent networking
Lineage II is a pretty old game, and as any sucessful MMORPG game, it has seen several updades over the years. In my code, my idea was to support any of them without branching the code. The main reason for it, comes from the fact that, as with anything, people hate changes. They tend to stick to the things they have come do enjoy and don't ever think of switching to new things. On Lineage II the same was true, people got used to Interlude and rulucted to move to any other version of the game.
This behavior proves to be a huge issue in software development, specially on opensource software - branching, which is the primary way to handle software versioning, proves to be too much work because back-porting all the updates to newer versions, stripping new client code from the source, is inviable as the code starts to grow.
The solution I have found for this issue? Service-Oriented Architecture and highly decoupling of services
The ServiceManager is the main entry point. It handles all the services running on a single application instance. Service implementations are configured on an XML file called
services.xml. Each entry on the XML has a interface and a implementation attribute which define, respectively the interface and implementation Java class. XML elements inside each of those services, are configurations sent directly to the service -- unifing the implementation definition and configuration.
Services can be stopped, started and restarted at runtime, this even allow services to be hot-swapped at any time. Hot-swapping is not recommended.
The ServiceManager is instructed to start all services (preferably in the order of declaration on the XML, however not required). Each service started pulls it's dependencies to be started and soon all services are started. Dependency Injection is handled by Google's Guice library. Services are splitted into 4 large domains:
- Core: Provides low-level abstractions over file system, threads, scheduling, caching, etc.
- Game: Handles all the game logic such as characters, NPC, attacking, skills, etc. Dispatches an event for any change or action.
- Database: Handles model storage. Provides several
DatabaseServiceinstances (MySQL, SQLite, OrientDB, etc).
- Network: Handles connections to physical game-clients and listens for
WorldEventDispatcherServiceevents that could be sent or broadcasted to the players.
Those services could be seen as tools.
The game domain is the largest of the 4. It handles all game logic such as characters, NPCs, attacking and other interactions -- it implements the game. What's more interesting is the decoupling applied here: this collection of services has no knowledge of networking, you can even use the server with no networking if you wish.
Every service that performs some kind of action, like attacking, moving, teleporting and so fourth, disptaches an event that can be listened by any other service. Event dispatching is handled by
WorldEventDispatcherService. It is important to note that events are dispatched by the
ThreadService (from Core domain). There are an equal number of threads as the number of processors on the server machine.
WorldListeners are not allowed to perform long or blocking operations.
As a design decision, any error when performing an action will throw an exception that will get caught at the network layer, which is invoking game services. This allows us to receive instant feedback on error while debugging and setup the correct error-code packet to send to the client. As a matter of fact, if an uncaught exception is thrown, when debug is enabled, a window appears in-game showing the details and the stacktrace!
It also makes use of many advanced design-patterns to make queries as simple as possible - no query is stored in a string, for SQL databases, it uses the QueryDSL library to provide a rich and dynamic query language for Java. OrientDB (NoSQL) database is also supported and provides a different abstraction build on top of QueryDSL table objects.
Automatic schema update is supported by the service. On every start, the service checks the state of the schema. If any column is found missing it is created at service start time. Columns created by the user are not excluded.
With the help of the Core domain, database query results can be cached. At the moment of this writing, queries are not cached, but this could easily be implemented with the help of cache services provided by the core domain.
The network domain translated
WorldEvents into network packets sent to the game client. It is an important piece in the whole server architecture - it is what allows the multiple client versions support work. Each client version is a new
NetworkService that can encode the events in it's own way. Even cooler, is the fact that you can run multiple network services and support more than one version at the same time. Supporting multiple version is very odd and can cause many issues both on the client and on the server -- the fact here is that the server is so dynamic that this is possible!
The whole service architecture is based on Netty a asynchronous network framework for Java. It allows very fast, high traffic and good troughtput networking. Every write or read happens on a background threadpool. Packet data is parsed and processed on a secondary background threadpool.
The domain also makes use of an advanced
WorldEventDispatcherService feature: filtered events. Only events that match a specific filtering rule are dispatched. For example a filter is created for every client that only listen events that occur near a certain range from his characters, avoiding the need to broadcast events that occured outside his sight range. Filters can be combined using basic operations like
AND, this allows filters to create a tree in which each composed filters asks its children whether they allow the event to dispatch.