Skip to content

Limbo architecture

ljacqu edited this page Mar 26, 2017 · 3 revisions

This is a technical description of how limbo players are handled. Please refer to Limbo players for a general, non-technical description.

Classes in bold are public-facing classes: these are the classes that are used outside of the limbo package to use the limbo feature. The remaining classes are used internally within the limbo package.

  • LimboPlayer is the object with the limbo properties
  • LimboService keeps LimboPlayer objects in memory and offers methods to the outside world
  • LimboServiceHelper is a util to LimboService and extracts some of the longer actions
  • LimboPlayerTaskManager is also a util to LimboService and handles most of the limbo tasks (see below)
  • LimboPersistence is used in LimboService to read and write LimboPlayers to disk
  • LimboPersistenceHandler is an interface with multiple implementations. The implementation that is chosen depends on the settings. The proper implementation is used within LimboPersistence to perform the actual I/O work. The used implementation in LimboPersistence is switched on each reload (new instance / new implementation if setting has changed).

Concept

The goal of the LimboPlayer is to remove certain features from the player until he logs in. These features are stored on the LimboPlayer. To be able to restore the values as they were, we keep them on the LimboPlayer.

A LimboPlayer is created each time a player joins the server, logs out or unregisters, i.e. whenever he is online on the server in an authenticated state. The values on the LimboPlayer are restored onto the player when he logs in, or if he quits the server.

Limbo tasks

Aside from removing certain features from the player when creating a LimboPlayer, we also start two tasks associated with the player.

  • MessageTask is a task run repeatedly telling the player to register or log in.
  • TimeoutTask ensures that the player is kicked after the configured number of seconds if he has failed to log in successfully.

When a LimboPlayer is restored (removed from memory), the tasks set on the object must be canceled.

Limbo player merge

It is possible that there exist multiple LimboPlayer objects for a player. In this case, we merge the data from both LimboPlayers, creating one LimboPlayer with the combined data.

Consider the following scenario: given a player that is not logged in, a LimboPlayer for him will be in memory. If an admin unregisters the player while he hasn't logged in, the creation of another LimboPlayer is triggered.

We try to keep the most relevant data by taking the maximum of each LimboPlayer: max speed, and if either is OP or allowed to fly, we take this over into the resulting LimboPlayer.

File persistence

Each LimboPlayer is typically also stored in the file system upon creation. In case the server crashes, AuthMe does not get the chance to restore the LimboPlayer data and it is lost from memory, resulting in players losing their speed, OP status, etc. Saving LimboPlayers to files solves this.

When a LimboPlayer is restored, the LimboPlayer is also removed from disk. When a new LimboPlayer is created, we first check the disk whether there is a LimboPlayer for the given player. If so, we again perform a merge of the LimboPlayer on disk and the newly created LimboPlayer object.

Persistence types

Persistence is configurable by the user in config.yml. We have the following implementations:

  • NoOpPersistenceHandler: "empty" class, does not perform any disk I/O.
  • SeparateFilePersistenceHandler: saves LimboPlayer objects into an individual file for each.
  • SingleFilePersistenceHandler: saves all limbo players into the same file, and keeps them in memory (after initialization, only writes and never reads)
  • SegmentFilesPersistenceHolder: saves limbo players into a configurable number of files. See below.

Segment files persistence

The segment files persistence saves all limbo players into a configurable number of files. Based on the player's UUID, a segment ID is created which is used as file name. Two values need to be defined to generate a segment ID: distribution and length.

UUIDs use hexadecimal characters, i.e. 0-9a-f — a total of 16 characters. The distribution defines onto how many outputs a single character should be mapped. With a distribution of 4, any character in 0-9a-f will be converted to one of four characters. This is done by converting the character from hexadecimal to decimal and then dividing it by (16 / distribution) and then rounding down the result. For an equal distribution, a power of 2 must be chosen as distribution.

For example:

distribution = 4, character = 'a'
--> output = hexToDec(character) / (16 / distribution) = 10 / (16 / 4) = '2'

The length defines how many characters of the UUID should be considered to create the segment ID. So a length of 3 and a distribution of 2 means that the first 3 characters will be mapped to 2 different outputs, creating 2^3 = 8 different possibilities:

Given: distribution = 2, length = 3, uuid = 7e32aa0e-6bd2-4779-a775-6258b79062e9
Take the first 3 characters, 7e3:

segmentId = floor(hex2dec(7) / (16/2)) . floor(hex2dec(e) / (16/2)) . floor(hex2dec(3) / (16/2))
          = floor(7/8) . floor(14/8) . floor(3/8) 
          = 010

A player with UUID 7e32aa0e-6bd2-4779-a775-6258b79062e9 is thus stored in a file with 010 in its name.

Other example:

Given: distribution = 8, length = 2, uuid = 6d205dc8-a0b3-42b0-85d4-5b87023a8ab7
Take the first 2 characters, 6d:

segmentId = floor(hex2dec(6) / (16/8)) . floor(hex2dec(d) / (16/8))
          = floor(6/2) . floor(13/2)
          = 36

For the available configurations, see the SegmentConfiguration enum.