/Javelin

A simple communication protocol for broadcasting events on a network.

Primary LanguageJavaGNU General Public License v3.0GPL-3.0

Javelin

Build status Mindustry 6.0 | 7.0 Xpdustry latest

Description

A simple and fast communication protocol for your internal network or Mindustry servers, enabling powerful features such as global chats, synced moderation, discord integrations, etc...

Runtime

This plugin is compatible with V6 and V7.

If you run on v135 or lower, you will need mod-loader for the dependency resolution.

Setup

This tutorial is aimed for non-advanced users looking to create their server network very easily :

  1. Install the plugin in all the servers you wish to link and start them, it will create the necessary config files in the ./javelin directory.

  2. Choose a Mindustry server that will host the main Javelin server.

    I suggest you to choose your hub server or the one that is the most stable.

    Go in the config file of the said server in ./javelin/config.properties and edit the following properties :

    • fr.xpdustry.javelin.socket.mode to SERVER.

    • fr.xpdustry.javelin.server.port : The port of your Javelin server (optional, default is 8080).

    • fr.xpdustry.javelin.socket.workers : The number of threads handling the incoming and outgoing events (optional).

    • fr.xpdustry.javelin.server.always-allow-local-connections : Allows clients to connect without a password if they are on the same network (optional, default is false).

    Then if you did not enable always-allow-local-connections or you did, but you have servers that aren't in the Javelin server network, you can add them with the command javelin-user-add <username> <password>.

    Users are saved in a binary file at ./javelin/users-v2.bin.gz, passwords are salted and hashed with Bcrypt.

  3. Once it's ready, restart your Mindustry server and your Javelin server should start along it.

  4. Now, for each server where Javelin is installed, edit the following properties in the config file at ./javelin/config.properties :

    • fr.xpdustry.javelin.socket.mode to CLIENT.

    • fr.xpdustry.javelin.client.address to the main javelin server address such as ws://example.org:port (if the client is in the network of the server and that always-allow-local-connections is enabled, set to ws://localhost:port).

    • fr.xpdustry.javelin.socket.workers : The number of threads handling the incoming and outgoing events (optional).

    If a password is required for the server :

    • fr.xpdustry.javelin.client.username to the username you assigned for this server.

    • fr.xpdustry.javelin.client.password to the password you assigned for this server.

  5. Restart all servers and enjoy the wonders of simple networking.

    Having problems ? Don't mind asking help to the maintainers in the #support channel of the Xpdustry Discord server.

Usage

Java

First, add this in your build.gradle :

repositories {
   maven { url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") }
   // If you want to use the snapshots, replace the uri with "https://maven.xpdustry.fr/snapshots"
   maven { url = uri("https://maven.xpdustry.fr/releases") }
}

dependencies {
    // Don't forget to suffix the version with "-SNAPSHOT" if using the snapshots
    compileOnly("fr.xpdustry:javelin-mindustry:1.2.0")
}

Then, update your plugin.json file with :

{
  "dependencies": [
    "xpdustry-javelin"
  ]
}

In your code, get the socket instance with JavelinPlugin.getJavelinSocket() (do not call it before init).

If you use ExtendedPlugin of Distributor, do not call before onLoad().

Now, you can subscribe to the incoming events with subscribe(event-class, subscriber) and send events with sendEvent(event).

Here is an example Plugin that can synchronize ban events :

public final class BanSynchronizer extends Plugin {

  @Override
  public void init() {
    // Get socket instance
    final JavelinSocket socket = JavelinPlugin.getJavelinSocket();

    Events.on(EventType.PlayerIpBanEvent.class, e -> {
      // If the socket is open, send the ban
      if (socket.getStatus() == JavelinSocket.Status.OPEN) {
        socket.sendEvent(new JavelinBanEvent(e.ip));
      }
    });

    socket.subscribe(JavelinBanEvent.class, e -> {
      // Ban player
      Vars.netServer.admins.banPlayerIP(e.getIP());
      // Kick player if connected
      final Player player = Groups.player.find(p -> p.ip().equals(e.getIP()));
      if (player != null) {
        player.kick(Packets.KickReason.banned);
      }
    });
  }

  public static final class JavelinBanEvent implements JavelinEvent {

    private final String ip;

    public JavelinBanEvent(final String ip) {
      this.ip = ip;
    }

    public String getIP() {
      return ip;
    }
  }
}

More info in the Javadoc.

JavaScript

For the giga chad programmers making plugins in JavaScript, Javelin is guaranteed to work in V7 (v127+). It can be used like in the java example by grabbing the instance on ServerLoadEvent with Vars.mods.getMod("xpdustry-javelin").main.

If you want to use java defined javelin events, here is an example :

// If you use "ModLoader" for your plugin depencency resolution, replace the line below with
// Vars.mods.getMod("xpdustry-mod-loader-plugin").main.getSharedClassLoader()
const loader = Vars.mods.mainLoader()
// Here we obtain the event class
const SomeEvent = java.lang.Class.forName("org.example.plugin.SomeEvent", true, loader)
// Now we can subscribe to the event
const javelin = Vars.mods.getMod("xpdustry-javelin").main
javelin.getJavelinSocket().subscribe(SomeEvent, event => {
  // Example usage
  Log.info(event.getSomeData())
})
// If you want to send the event, you will need to do it with a relfective operation
// because "new" doesn't work with classes that are obtained with "Class.forName" and not "Packages"
let event = SomeEvent.getConstructor(java.lang.String).newInstance("some-name")
javelin.getJavelinSocket().sendEvent(event)

Now, if you want to define your own events in JavaScript, you can use the provided JavelinJsonEvent with some wrapper code :

// see comment above
const loader = Vars.mods.mainLoader()
const JavelinJsonEvent = java.lang.Class.forName("fr.xpdustry.javelin.JavelinJsonEvent", true, loader)
const javelin = Vars.mods.getMod("xpdustry-javelin").main

function sendEvent(name, event) {
  const json = JavelinJsonEvent.getConstructors()[0].newInstance(name, JSON.stringify(event))
  javelin.getJavelinSocket().sendEvent(json)
}

function subscribe(name, subscriber) {
  javelin.getJavelinSocket().subscribe(JavelinJsonEvent, event => {
    if (event.getName().equals(name)) subscriber(JSON.parse(event.getJson()))
  })
}
    
subscribe("event-name", event => {
   // Example usage
   Log.info(event.data)
})

sendEvent("event-name", {
  data: "Hello"
})

Tips

  • The socket isn't reusable, do not start nor close it yourself !!!

  • Javelin does not support Arc collections such as Seq, ObjectMap, ... (it may still work, but it will be terribly optimized).

  • You can add wss support on javelin with a reverse proxy like nginx. Example with certbot :

    # This is required to upgrade the websocket connection
    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }
    
    upstream javelin {
        # The address and port of your javelin server
        server example.org:8080;
        keepalive 64;
    }
    
    server {
        # The name of your server (example: javelin.example.org)
        server_name javelin.example.org;
    
        listen 443 ssl;
        listen [::]:443 ssl;
    
        # Don't forget to change that
        access_log /var/log/nginx/javelin.example.org-access.log;
        error_log /var/log/nginx/javelin.example.org-error.log;
    
        location / {
            proxy_pass         http://javelin;
            proxy_set_header   Host              $host;
            proxy_set_header   X-Real-IP         $remote_addr;
            proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;
            proxy_set_header   Upgrade           $http_upgrade;
            proxy_set_header   Connection        $connection_upgrade;
            proxy_http_version 1.1;
        }
    
        # This part is generated by certbot, so replace it with your own
        # Managed by Certbot
        ssl_certificate /etc/letsencrypt/live/javelin.example.org/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/javelin.example.org/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    }

    Now, your javelin server is accessible with wss://javelin.example.org/.

Building

  • ./gradlew :javelin-mindustry:jar for a simple jar that contains only the plugin code.

  • ./gradlew :javelin-mindustry:shadowJar for a fatJar that contains the plugin and its dependencies (use this for your server).

Testing

  • ./gradlew :javelin-mindustry:runMindustryClient: Run the plugin in Mindustry desktop.

  • ./gradlew :javelin-mindustry:runMindustryServer: Run the plugin in Mindustry server.