IAN is an unofficial Java library for communicating with Artemis Spaceship Bridge Simulator servers and clients.
IAN is a heavily revamped version of ArtClientLib. As of this writing (7 Oct 2022), it supports Artemis versions 2.7.5 and later. If you need Java library support for previous versions of Artemis, please see the version table below:
| Artemis | IAN/ArtClientLib |
|---|---|
| 2.7.0-4 | IAN 3.4.0 |
| 2.4.0 | IAN 3.2.1 |
| 2.3.0 | IAN 3.0.0 |
| 2.1.1 | ArtClientLib v2.6.0 |
| 2.1 | ArtClientLib v2.4.0 |
| 2.0 | ArtClientLib v2.3.0 |
| 1.x | ArtClientLib v1.x |
Note, however, that these previous versions will be missing various bug fixes and enhancements found in the latest version.
This library was originally developed by Daniel Leong and released on GitHub with permission of the developer of Artemis, Thom Robertson.
IAN is an unofficial library. Thom Robertson and Incandescent Workshop are not involved with its development and do not provide support for it. Please don't bother them with questions about it.
Stuff might not work. There is no official public documentation of the Artemis protocol. A library such as IAN is made possible through reverse engineering, requiring careful observation of Artemis network traffic and experimentation to learn how game data is formatted. IAN is a very good implementation of the Artemis protocol, but under these circumstances it is next to impossible to guarantee that it is 100% accurate. Also, new Artemis releases are likely to break this library until a contributor can figure out what changes were made and update things accordingly.
There is no official support. Bug reports and feature requests are welcome, and I'm happy to try to answer questions. However, IAN is developed in my spare time, and as such, I can't guarantee support for it. I can't make any promises to address your requests, bug reports or questions in a timely fashion.
Use at your own risk. This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; not even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Don't pirate Artemis. IAN is intended to allow you to enhance your Artemis experience with custom behavior. However, using custom Artemis software does not release you from the obligation to license the original Artemis software. If you are playing Artemis without a license, either with stock or custom software, you are engaging in software piracy and inhibiting the future development of Artemis. If you don't have a license, please support Thom by purchasing one now.
I'd love to see what you do with it! If you make something cool from this, I'd love to know. Crediting this library would be appreciated, as would sharing any improvements you make, to potentially include upstream contributions in the form of pull requests.
You can help make IAN better. There are several ways that you can assist in IAN's development. See IAN's contribution guidelines for details.
The com.walkertribe.ian.example package contains some simple demonstration classes to help you see how to use IAN:
AnnounceServerDemo: Shows how to respond to server discovery requests from clients so that you can make your server or proxy discoverable on the LAN.ClientDemo: Sample client that toggles your ship's red alert state when shields are raised or lowered.FindServersDemo: Shows how to discover servers on the LAN.ProxyDemo: Simple proxy that suppresses toggling red alert by any client that connects through it.
See
FindServersDemofor example code.
-
Implement the
ServerDiscoveryRequester.Listenerinterface. This object will be notified whenever a server is discovered and when the discovery wait time ends. -
Construct a
ServerDiscoveryRequesterand pass it yourListener. You'll also need to specify how long you want to wait for responses to your discovery request. -
Run the
ServerDiscoveryRequesterinstance. To do this, construct a newThread, passing in theServerDiscoveryRequesterinstance, then callstart()on theThread. While theThreadis running,Listener.onDiscovered()will be invoked for each server it finds. When the wait time expires, it will invokeListener.onStop(). -
Reuse the
ServerDiscoveryRequesteras needed. TheServerDiscoveryRequesterwill only send a single request, when theThreadfirst starts. If you want to continually poll for servers, wait for theServerDiscoveryRequesterto stop, then spin up anotherThreadwith the same requester object.
See
ClientDemofor example code.
-
Instantiate an object which implements
ArtemisNetworkInterface. This object is responsible for managing the connection to the Artemis server. If you have aServerobject from aServerDiscoveryRequester(see the previous section), you can just invokeconnect()on theServer, which will return anArtemisNetworkInterface. Otherwise, you must construct aThreadedArtemisNetworkInterfaceobject (which implementsArtemisNetworkInterface). You must provide the constructor with the host/IP address and port to which it should connect. (By default, Artemis servers listen for connections on port 2010, but this can be changed in Artemis'sartemis.inifile.) On construction, it will attempt to connect, throwing anIOExceptionif it fails. -
Add event listeners. Next, you must add one or more event listeners to the
ArtemisNetworkInterfacevia theaddListener()method. Event listeners are objects which contain methods marked with the@Listenerannotation. IAN will notify these listeners when certain events occur. See the Event Listeners section below for more details. -
Start the network interface. Once your listeners are registered, invoke
start()on theArtemisNetworkInterfaceobject. Once you invokestart(), your listeners will start being notified of events. -
Send packets in response to events. As your listeners receive events, you will inevitably want to send packets back to the Artemis server. To do so, pass an instance of the appropriate class that implements
ArtemisPacketto theArtemisNetworkInterface.send()method. Here's an example:private ArtemisNetworkInterface iface; /** * Fire whatever's in the given tube. */ private void fireTorpedo(int tubeIndex) { iface.send(new FireTubePacket(tubeIndex)); }
-
Disconnect. To disconnect from the Artemis server, invoke the
ArtemisNetworkInterface.stop()method. Make sure you do this so that the send and receive threads will be terminated; otherwise, your application won't stop.
-
Listen for a client connection. Open a
ServerSocketon the desired port, then callaccept()on it to listen for a connecting client. Theaccept()method will block until a client connects or it times out, and return aSocketobject when the client connects. You can set the timeout by callingServerSocket.setSoTimeout(); passing in0will cause it to wait indefinitely for a connection. -
Wrap the client
Socketin aThreadedArtemisNetworkInterfaceobject.ThreadedArtemisNetworkInterfacehas a constructor that accepts aSocketand anOrigin(CLIENTin this case). The resulting object will be responsible for managing the connection to the client. -
Use the
ArtemisNetworkInterfacethe same way you do with clients. You can now add listeners, start the interface, send packets to the client, and disconnect just as you do when connected to a server. See "Connecting to an Artemis Server" above, starting at step 2.
See
ProxyDemofor example code.
-
Listen for client connections. See "Accepting an Artemis Client Connection" above.
-
When you receive a connection, connect to the Artemis server for which you are proxying. This is done exactly the same way as you would for creating an Artemis client, as documented in "Connecting to an Artemis Server". You now have two
ArtemisNetworkInterfaces: one for the client and one for the server. -
Pass through all packets. The
proxyTo()method onArtemisNetworkInterfacecreates a "pass through" connection between two connections of opposite origin types. Any packet that is not caught by a listener method will be passed through automatically. Note thatproxyTo()only creates connection in one direction; to pass through packets both ways, each connection will need to callproxyTo()on the other. -
Turn off auto-heartbeat. By default,
ArtemisNetworkInterfacewill automatically send periodic heartbeat packets so that the remote machine knows that it's still alive. However, in this scenario, your proxy is going to automatically forward the heartbeat packets it receives, so it doesn't need to send its own. So you will want to turn off automatic heartbeat packet sending on both sides. To do this, callsetAutoSendHeartbeat(false). (Note: If for some reason you are actually listening for heartbeat packets, you will need to pass them along manually.) -
Add listeners. Add your listeners to both the client and server interface objects. Once the listener has caught the packet and extracted whatever information it wants from it, you can either send the packet along by passing it to
send(), suppress it (by doing nothing), or even inject your own packets instead (by constructing them and passing them tosend()). Remember thatproxyTo()does not pass along packets caught by listeners, so it's up to you tosend()them if you want them passed along. Also, keep in mind that multiple listeners can catch the same packet; be careful not to send it more than once! -
Start both network interfaces. Invoke
start()on both of them to start consuming packets from both connections. -
When one side disconnects, close the connection to the other side. Listen for the
DisconnectEventfrom both sides. When you receive it from one side, invokeArtemisNetworkInterface.stop()on the other connection (or both connections, if that's easier; callingstop()on an already closed connection has no effect).
See
AnnounceServerDemofor example code.
-
Construct a
ServerDiscoveryResponder. You can either determine the IP and host name to announce yourself (in which case, it's up to you to ensure they are correct), or you can use the no-arg constructor and allow IAN to attempt to select the appropriate network interface to use. Only active, non-loopback interfaces where the address corresponds to an IPv4 private network will be considered. If no suitable interface is found, anIOExceptionwill be thrown with the message "No suitable network interface found." Those that meet these criteria will be sorted, first by whether or not it is a wifi interface (with non-wifi preferred), then by class (A, B, then C). The most preferred interface will be selected. -
Run the
ServerDiscoveryResponderinstance. To do this, construct a newThread, passing in theServerDiscoveryResponderinstance, then callstart()on theThread. While theThreadis running, theServerDiscoveryResponderwill automatically respond to any discovery requests it receives. -
Invoke
stop()when you are no longer accepting client connections. You don't want to continue to announce your server's presence to the LAN if you're not accepting client connections anymore. Callingstop()on theServerDiscoveryResponderwill cause it to shut down theThreadso that it will stop responding to requests.
To make your application react to events, you must write one or more event listeners and register them with your ThreadedArtemisNetworkInterface object via the addListener() method. An event listener can be any Object which has one or more methods marked with the @Listener annotation. A listener method must be public, return void, and have exactly one argument. The type of that argument indicates what sort of events will cause the method to be invoked:
ConnectionEvent: Events related to IAN's connection with the remote machine or its heartbeat.ArtemisPacket: IAN receives a packet from the remote machine.ArtemisObject: IAN receives anArtemisObjectinside anObjectUpdatePacket.
You should use the most specific subtype you can for the argument type, as IAN will only invoke your listener method when the type of the argument matches. For example, if you write a listener method with an argument of type CommsIncomingPacket, it will be invoked only when that packet type is received (when the COMMs station receives an incoming text message). IAN will only bother to parse a packet if a listener is interested in it, so writing your listener methods to specific subtypes can be significantly more efficient. If you write a listener that has ArtemisPacket as its argument, IAN will parse all packets it receives because as far as it knows, you're interested in all of them.
One important event to listen for when creating a client is the ConnectionSuccessEvent. You shouldn't attempt to send any packets to the server before you receive this event. This may also be a good time to send a SetShipPacket and a SetConsolePacket.
It's also likely that you'll want to know when the connection to the remote machine is lost; listening for DisconnectEvent will handle that.
Artemis servers and clients send occasional heartbeat packets as a way of letting remote machines know they're still alive. The ThreadedArtemisNetworkInterface will raise a HeartbeatLostEvent when a heartbeat packet has not been received recently. If afterwards a heartbeat packet is received, a HeartbeatRegainedEvent will be raised.
The ArtemisObject interface is implemented by objects which represent game world objects (ships, bases, creatures, asteroids, torpedoes, black holes, etc.). The server will frequently send ObjectUpdatePackets that contain these objects.
ArtemisObjects have properties which are exposed with getters and setters. The properties which are available will vary depending on the object's type. In order to save network bandwidth, the server will usually send only the properties of an object that have changed since the last update rather than the entire state of the object. Therefore, these properties may be unspecified.
Rather than use null to mean "unspecified" (and incur the memory overhead of instantiating many primitive wrapper objects), IAN uses a specific value for each property to indicate that it is unspecified. The value that means "unspecified" depends on the data type:
| Data type | Unspecified value |
|---|---|
BoolState |
BoolState.UNKNOWN |
other Objects |
null |
float |
Float.NaN |
byte or int |
-1 or MIN_VALUE |
For byte or int, -1 is used to mean "unspecified" unless -1 is a valid value. In that case, MIN_VALUE is used. Any property which does not follow the above rules will explain in its documentation.
The ArtemisObject.updateFrom() method copies the specified properties from one object to another. This allows you to keep an object current as you recieve updates: simply pass the updated object to the original object's updateFrom() method and will have the updates applied to it. If you wish to maintain updated status for the entire game world, the World class handles this for you. Simply instantiate one and register it as a listener with your ArtemisNetworkInterface. As the simulation runs, you can use the various get*() methods to retrieve game state.
Apart from world objects, another common state you may want to track would be a ship's system grid. There are two distinct parts to the grid:
- Infrastructure data: physical locations of nodes and what ship systems are located there
- Current state: damage, status of DAMCON teams
The grid consists of a 5 × 5 × 10 matrix of nodes overlaid on the ship. A node can be a ship system, a hallway, or empty (typically because it falls outside the ship). DAMCON teams can move between any two adjacent non-empty nodes in the grid.
Each node has two sets of coordinates:
- Grid coordinate: Its location in the 5 × 5 × 10 matrix described above. These coordinate values are
ints. - Physical coordinate: Its physical location relative to the center of the ship. These coordinate values are
floats.
The Grid class is used to track system grid information, and it can store either or both of the kinds of data mentioned above. When you have a Vessel object (described in "Locating Artemis Resources" below), you can retrieve the infrastructure grid from it by calling getGrid(). The Grid class also has @Listener methods that will update it with damage and DAMCON information as the simulation runs when you register it with the ArtemisNetworkInterface.
Note that Grid objects you retrieve from Vessels are "locked", meaning they cannot be modified. (This is because data read from vesselData.xml data is cached for reuse.) If you want to use a Grid from a Vesselto start tracking state, you will need to clone the Grid and work from the clone. Grid has a constructor that does this for you.
There are several ways to retrieve node data from a Grid:
- To access an individual node, you will need a
GridCoord. These are statically cached; you can retrieve one by callingGridCoord.get(x, y, z), passing in its grid coordinates. Once you have theGridCoordobject, retrieve its corresponding node by passing it toGrid.get(GridCoord). GridimplementsIterable<GridNode>, so you can iterate it to visit all theGridNodesin theGrid.- You can also invoke
getAllNodes()to return aListof all the nodes. - You likely are really only interested in the accessible nodes;
getAccessibleNodes()will provide those. - If you would like the nodes grouped by the
ShipSystemthey contain, callgroupNodesBySystem(), which will return aMap<ShipSystem, List<GridNode>>. Note that empty and hallway nodes are omitted from the returnedMap.
Unless you use one of the constructors that prepoplate the Grid, a new Grid starts out with all 250 nodes being empty. If the Grid is not locked, you can replace any node with a new GridNode with whatever infrastructure data you specify. You do this by calling Grid.set(GridNode). To copy all the node data from one Grid to another, use copyNodes(Grid, boolean).
To retrieve an individual DAMCON team, call getDamcon(byte), passing in the team's ID. To get a new List of all DAMCON teams, call getAllDamconTeams().
To update a DAMCON team, construct a DamconTeam with the new data, then pass it to setDamcon(DamconTeam). To copy all DAMCON team data from one Grid into another, call copyDamcons(Grid). Note that these methods will fail if the Grid is locked.
When rendering a 3D image of the Grid, you will need to be able to get the locations of all nodes and DAMCON teams so that you can easily transform them prior to rendering. The toPointCloud() method does this, returning a Map<String, Point> containing the Points for all non-empty nodes and DAMCON teams. Then you can retrieve a Point by providing its key:
Points corresponding toGridNodes have keys in'[x,y,z]'format, like this:'[1,5,7]'.Points for DAMCON teams have keys in'DAMCON n', format, like this:'DAMCON 1'
It's particularly useful to pass this into Model.transformPoints(), allowing you to apply a transformation to them so that you can render them on top of a transformed Model. This lets you render a 3D ship with the system grid overlaid on top.
Some operations require data which is obtained from Artemis resource files. There are three major classes of resource data IAN can make use of:
| IAN class | Artemis files | Contents |
|---|---|---|
VesselData |
dat/vesselData.xml |
Factions and Vessels |
Model |
dat/*.dxs |
3D mesh |
Grid |
dat/*.snt |
Vessel system grid |
All three of these resource types are optional: as long as you don't need the particular functionality that is dependent on these resource files, IAN can run without them. Note that incompatibilities may result if the remote machine's version of the resource data differs from IAN's.
IAN provides several interfaces and classes that can help you with reading Artemis resource data:
-
Context: An interface for classes which can provide instances ofVesselData,ModelandGridto IAN. Any method that requires data from Artemis resource files will ask for an object which implementsContext. -
DefaultContext: An implementation ofContextwhich can parse Artemis resource data from anInputStreamand cache the results for future use. It delegates the creation of theInputStreamto aPathResolver. -
PathResolver: An interface for classes which can provide anInputStreamthat corresponds to an Artemis resource path. -
FilePathResolver: APathResolverimplementation that loads resources from the file system relative to a particular directory. You would typically point this at the Artemis install directory, but at most IAN will only look fordat/vesselData.xml,dat/*.dxs, anddat/*.snt; any other files will be ignored. -
ClasspathResolver: APathResolverimplementation that loads resources from the classpath relative to a specific class. This allows you to bundle the needed resources inside your JAR.
You can provide your own implementations of Context or PathResolver if the out-of-the-box implementations do not serve your purpose. For example, you could create a custom PathResolver implementation that could load Artemis resource data from a remote server. Or you can create a custom Context implementation that builds the data from scratch instead of parsing it from an InputStream. (In fact, that's exactly what the testing code does to exercise code that uses a Context.)
The vesselData.xml file contains information about the game's factions and vessels. The ArtemisShielded interface includes a getVessel(Context) method which will return a Vessel object. The ArtemisBase, ArtemisNpc, and ArtemisPlayer classes implement this interface. Vessel has a getFaction() method which will return the Faction corresponding to that Vessel.
Faction and Vessel data can also be retrieved directly from the VesselData object, which you can get by calling Context.getVesselData(). You can then invoke getFaction(int) or getVessel(int) to retrieve the Faction or Vessel with the corresponding ID. You can also iterate all the available Factions or Vessels with the factionIterator() and vesselIterator() methods.
DefaultContext loads resource files on demand and caches them for subsequent requests. If you wish to preload all resources up front, you can do so with the preloadModels() and preloadGrids() methods.
Note that whenever you attempt to get a reference to a single Faction or Vessel, IAN may return null. This occurs when the VesselData contains no Faction or Vessel that corresponds to the given ID. This may occur when the remote machine's vesselData.xml file doesn't match yours. You should make sure that your code handles this scenario gracefully.
Thanks to Thom Robertson and Incandescent Workshop for creating Artemis, and for so graciously tolerating the community's desire to tinker with their baby.
Daniel Leong (@dhleong) created the original version of ArtClientLib and did a lot of the difficult, thankless gruntwork of reverse engineering the Artemis protocol.
Various GitHub users have contributed to IAN by helping to discover and document the protocol, creating pull requests to implement features or bug fixes, or issuing bug reports. Their help is very much appreciated. They are, in alphabetical order: