Giter Club home page Giter Club logo

psf-loginserver's Introduction

PSForever Server Build Status Code coverage Documentation

Welcome to the recreated login and world servers for PlanetSide 1. We are a community of players and developers who took it upon ourselves to preserve PlanetSide 1's unique gameplay and history forever.

The login and world servers (this repo runs both by default) are built to work with PlanetSide version 3.15.84.0. Anything older is not guaranteed to work. Currently, there are no binary releases of the server as the state is pre-alpha alpha beta scare. To contribute, you will need to have a development environment in order to get it running. If you just want to play, you don't need the development environment. Join the public test server by following the PSForever Server Connection Guide, which has the instructions on downloading the game and using the PSForever launcher to start the game.

Server Requirements

  • Running
    • Java Development Kit 8.0
      • JDK 1.8_251
    • Scala
      • 2.13.3 (set in build.sbt)
    • sbt (Scala build tool)
      • 1.4.5+?
      • Up to date
    • PostgreSQL
      • 10+
  • Development (+Running)
    • Git
    • IDE or Text Editor

Git

Git means "Global Information Tracker" and is used for project revision control, keeping track of changes made to files. Git is not actually necessary to acquire the project as GitHub allows for downloading a ZIP archive of the source code that can be deployed anywhere. Development on the project, however, will require the use of GitHub and, as recommended, a fork of the main project repository.

Though git is traditionally a command line interface (CLI) application, GitHub offers a Desktop GUI that introduces graphical components and menus for normal operations, as well as visualization of the revision hierarchy itself. Depending on the Linux distro, users have access to the git CLI through a variety of commands. For Windows, use an appropriate installer for the CLI. Additionally, posh-git exists to make the CLI prettier and more informative for Windows PowerShell users.

Your IDE of choice may also be integrated with git. Check its documentation.

Use git clone https://github.com/[user]/PSF-LoginServer.git, where "user" is your GitHub account or psforever for the main project, to create a local copy. If the main project, you should not use the produced local repository for developmental purposes - fork it first and git clone that. One way or another, an installation directory of the project will have been created.

Languages

PSF-LoginServer is written in Scala and built using sbt which allows it to be built on any platform. sbt is the Scala version of Make, but is more powerful as build definitions are written in Scala. sbt is distributed as a Java JAR and the only dependency it has is a JDK. In order to compile scala, scalac is used behind the scenes. This is equivalent to Java's javac, and the language itself runs on top of the Java Virtual Machine, meaning it generates .class and .jar files and uses the java executable. Essentially, Scala is just a compiler that targets a JVM runtime.

Download and install the correct version of Java, a more up-to-date version of Java (if you don't have one), then follow the quick instructions on Scala's home page to get a working development environment. The modern installation of Scala utilizes Coursier, a Scala application used for dependency resolution, and will install a number of "apps" including the sbt build tool. These "apps" are always installed with the most-recent non-developmental versioning, so follow the command guidelines to install the version that is wanted or required. Additionally, specific releases of Scala can also be installed separately.

If you have Docker and docker-compose installed on your system, you can get a complete development environment up by running docker-compose up in the source code directory. Otherwise, keep reading.

sbt

As mentioned, when acquiring the Scala language using Coursier, a version of sbt will also be added. If not using Coursier, download sbt for your platform and install or extract it. Open up a command line tool - cmd.exe, bash, CYGWIN, Git Bash - that has the Java Development Kit accessible from prompt to use sbt commands.

Note: sbt is quite slow at starting up due to JVM JIT warmup. An open sbt console - just run sbt without any arguments - is recommended in order to avoid this startup time.

PostgreSQL Database

A database is required for persistence of game state and player characters. The login server and game server (which are considered the same things, more or else) are set up to accept queries to a PostgreSQL server. It doesn't matter if you don't understand what PostgreSQL actually is compared to MySQL. I don't get it either - just install it: for Windows; for Linux Debian, for Linux Ubuntu; or, for macOS, normally, or usingbrew install postgresql && brew net.psforever.services start postgresql.

Additionally, loading the database information will require access to a graphical tool such as pgAdmin (highly recommended) or a PostgreSQL terminal (psql) for advanced users. The Windows PostgreSQL installation will come with a version of pgAdmin.

To use pgAdmin, run the appropriate binary to start the pgAdmin server. Depending on the version, a tab in your web browser will open, or maybe a dedicated application window will open. Either way, create necessary passwords during the first login, then enter the connection details that were used during the PostgreSQL installation. When connected, expand the tree and right click on "Databases", menu -> Create... -> Database. Enter name as "psforever", then Save. Right click on the psforever database, menu -> Query Tool... Copy and paste the commands below, then hit the "Play/Run" button. The user should be created and made owner of the database. (Prior to that, it should be "postgresql".) (Check menu -> Properties to confirm. May need to refresh first to see these changes.)

CREATE USER psforever;
ALTER USER psforever WITH PASSWORD 'psforever';
ALTER DATABASE psforever OWNER TO psforever;

IMPORTANT NOTE: applying privileges after importing the schema will not apply them to existing objects. If this happens, drop all objects and try again or apply permissions to everything manually using the Query Tool / psql.

Using an IDE

Scala code can be fairly complex, and a good IDE helps you understand the code and what methods are available for certain types, especially as you are learning the language. IntelliJ IDEA has some of the most mature support for Scala of any IDE today. It has advanced type introspection (examine the properties of an object at runtime) and excellent code completion (examine the code as you are writing it). Download the community edition of IDEA directly from IntelliJ's website then get the required Scala plugin for IDEA.

You will need to import the project into the IDE. Older versions of IDEA (2016.3.4, etc.) have an import procedure where it is necessary to instruct the IDE what kind of project is being imported. Modern IDEA (2022.1.3) still utilizes this procedure but can also open the repo as a project and contextually determine what is being expressed by the code (much better than older versions can, anyway). Certain aspects will need to be clarified manually if this method is utilized, e.g., the JDK and JRE, since those choices were skipped. When the project is fully imported, create a new run configuration using the dropdown near the top of the interface. Create an sbt task configuration with the specific task instructions server/run. Confirm. Press the green arrow "Run") to launch the server.

Using a Text Editor

If you are not a fan of big clunky IDEs, and IDEA is definitely one of them, you can opt to use your favorite text editor - VSCode, Sublime, ViM, Notepad++, Atom, etc - and use sbt to build the project. The only dependencies necessary are sbt and access to the JDK. Everything else should be deployed from working with those. Run commands in a command line tool with appropriate dependency visibility to start the server.

Running the Server

The initial compile may take some time. Sbt is powerful but slow to wake up.

Startup

To run a headless, non-interactive server, run

sbt server/run

PlanetSide can now connect to your server. To run your custom server with an interactive scala> REPL, run

sbt server/console

image

To start the server and begin listening for connections, enter the following expression into the REPL:

Server.run

image

This process is identical to running the headless, non-interactive server: PlanetSide clients can connect, logging output will be printed to the screen, etc. The advantage is that you now have an interactive REPL that will evaluate any Scala expression you type into it. image

The REPL supports various useful commands. For example, to see the type of an arbitrary expression foo, run :type foo. To print all members of a type, run :javap -p some-type. You can run :help to see a full list of commands. image

The game server will automatically apply the latest schema to the database, updating sequential entries found in resources/db/migrations. Migrations can also be applied manually using the Flyway CLI. Existing databases before the introduction of migrations must be baselined using the flyway baseline command.

Tests

Tests that are packaged with the server project code can also be run using the command sbt test. The anticipated /test/ directory should be reachable under the /src/ directory. IntelliJ IDEA allows hinting of the directory from the context menu of the Project tab: menu -> Mark Directory as -> Test Sources Root or Test Resources Root.

GM

By default users are not granted game moderator privileges, colloquially referred to as a customer service representative (CSR). To grant a created user GM, access execute the following query:

UPDATE account SET gm=true WHERE id=[your_id];

You can find your account id by viewing the accounts table.

SELECT id FROM account WHERE username=[your_username];

Creating a Release

If you want to test the project without an IDE or deploy it to a server for run, you can use sbt-pack to create a release (included with the repository). First make sure you have the sbt tool on your command line (or create a new task in IntelliJ IDEA). Then get a copy of the source directory (either in ZIP or cloned form). Then do the below

cd PSF-LoginServer
sbt packArchiveZip # creates a single zip with resources

This will use the sbt-pack plugin to create a JAR file and some helper scripts to run the server. The output for this will be in the PSF-LoginServer/target directory. Now you can copy the ZIP file to a server you want to run it on. You will need the Java 8 runtime (JRE only) on the target to run this. In the ZIP file, there is a bin/ directory with some helper scripts. Run the correct file for your platform (.BAT for Windows and shell script for Unix).

Troubleshooting

  1. If dependency resolution results in certificate issues or generates a /null/ directory into which some library files are placed, the Java versioning is incorrectly applied. Your system's Java, via JAVA_HOME environment variable, must be advanced enough to operate the toolset and only the project itself requires JDK 8. Check that project settings import and utilize Java 1.8_251. Perform normal generated file cleanup, e.g., sbt's clean. Any extraneous folders may also be deleted without issue.
  2. If the server repeatedly complains that "authentication method 10 not supported" during startup, your PostgreSQL database does not support scram-sha-256 authentication. Check in your database configuration file postgresql.conf that password_encryption is set correctly; or, upgrade your PostgreSQL version to one that supports scram-sha-256. Whenever changing password encryption methods, all existing passwords FOR THE USERS AND ROLES should be rehashed for the new encryption.

Tools

decodePackets

The decodePackets program can be used to decode GameLogger .gcap packet captures. Requires gcapy to run, unless the -p flag is used.

To build, run:

sbt decodePackets/pack

The output will be in tools/decode-packets/target/pack. The bin folder contains scripts to launch the program. On Linux, you can use the Makefile to install the files to any path:

make install PREFIX=$HOME/.local

Now you can run the program like that:

psf-decode-packets -o ./output-directory -f foo.gcap bar.gcap

By default, decodePackets takes in .gcap files, but it can also take gcapy ascii files with the -p option. Run psf-decode-packets --help to get usage info.

Generating Documentation

Using sbt, you can generate documentation all projects using sbt docs/unidoc. Current documentation is available at https://psforever.github.io/PSF-LoginServer/net/psforever/index.html

Contributing

Please fork the project and provide a pull request to contribute code. Coding guidelines and contribution checklists coming soon.

Get in touch

License

GNU GPLv3. See LICENSE.md for the full copy.

psf-loginserver's People

Contributors

adamlc avatar aphedox avatar fate-jh avatar ivanwick avatar jgillich avatar kingferaligatr avatar lelfjior avatar ltripley36706 avatar mazo avatar mjsmith707 avatar nickpsf avatar pschord avatar renovate-bot avatar resaec avatar scrawnyronnie avatar sounours avatar tfarley avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

psf-loginserver's Issues

Integrate MariaDB connector in to the server

We have settled on MariaDB for our database. This is essentially feature equivalent with MySQL but more open source (different philosophy).

Now we need

  • A Scala library that works well with MariaDB / MySQL. We DO NOT want a typesafe super-scala crazyness library as it will probably be much harder to read and our SQL will not be portable
  • An understanding of how JDBC fits in to all of this
  • Some schemas to start building (login session, world session, world list, character table, world state)

Handling Strings without Length

Having done some packet diving, I've discovered at least two packets that pass ASCII in a wide character format, but do not have the length prefacing the string directly. We have no prepared encoding or decoding for this kind of data as they all assume the first byte or pair of bytes are string length, and this means we can not build packet handling logic. The straightforward aspect of the packet handling also doesn't leave much room for interjection.

For example, a DestroyDisplayMessage packet looks like this:
81 8741006E00670065006C006C006F0035BC D801 8F 20 19207 0A 048004D00460049004300B18E D901 00
There are two character strings here. For the first string, 87 is the proper length (well, 7 is) so the whole thing can be decoded normally. For the second string, however, the length is actually encoded in the 0A in front of the lone 0 as twice the length of the string (5 x 2 = 10, or 0A; modifying it produces predictable results). The number format is incorrect, and the buffering 0 just throws the whole thing off.

In addition, passing DestroyDisplayMessage with the correct form of the encoded wide character string to the client - 8548004D00460049004300 - causes the client to display illegible pictograms in place of the string.

Here's an example of a DamageFeedbackMessage:
7B 76 1448004D0046004900430086 03 90 50 00 00 00
The wide character string is obvious but, this time, the only suggestions of the size exists as high order bits towards the far end of the data. Whether or not that's what it is, we can't deal with it. (A five letter wide character string also has twenty nibbles and 14 is 20 ... I just made that up on the spot.)

Chat - Various Chat Issues

  • /squad is currently global for team, despite Squads being implemented. We use /platoon on Test Live 51200 for Global to Team.

  • /platoon is not implemented for the proper Global to Team.

  • /broadcast is the default message, currently. On live, it was Local. The expected behavior for Master is currently Squad or Platoon, preferably Platoon, as the Global to Team chat.

  • Voice chat text is issued twice clientside.

This is the current setup for 51200 and it's pretty important to move this to Master.

LoginMessage packet does not handle token

When attempting to decode this appears

Failed to decode encrypted packet: Failed to parse control packet 0x07: Could not find a marshaller for control packet Unknown7
Failed to decode inner packet Failed to parse game packet 0x01: credential_choice: US-ASCII cannot decode string from '0x484f35366e51706c6c73705046395632000000000000000000b8060390b80603'
Failed to decode inner packet Failed to parse game packet 0x01: credential_choice: US-ASCII cannot decode string from '0x484f35366e51706c6c73705046395632000000000000000000b8060390b80603'

Looks like it could have something to do with the fixedSizedByte combinator in scodec (see

object LoginMessage extends Marshallable[LoginMessage] {
private def username = PacketHelpers.encodedStringAligned(7)
private def password = PacketHelpers.encodedString
private def tokenPath = fixedSizeBytes(32, bytes) :: username
private def passwordPath = username :: password
)

Update README.md with contact informations

It would be nice when the README.md would contain information how to get in contact with the dev team (e.g. Discord or the website).

This issue is also relevant for other repositories like the world server.

VNLWorldStatusMessage cannot handle multiple worlds

PlanetSide packet decoding is so next-gen that string decoding can occur on non-byte aligned boundaries. Scodec doesnt like this (especially since codecs are structural, not dynamic in nature) and is not able to decode a second world:

("worlds" | vectorOfN(uint8L, (
// XXX: this needs to be limited to 0x20 bytes
// XXX: this needs to be byte aligned, but not sure how to do this
("world_name" | PacketHelpers.encodedString) :: (

This reason this occurs is that the end of the world record within the VNLWorldStatusMessage is not byte aligned. On the second go around for a new world record, the bitvector is misaligned. Within the PlanetSide client there is an alignment operation performed right after the length of the following string is read. This is okay, but the VNLWorldStatusMessage isn't written for a misalignment. This leads to the behavior that the first entry decodes fine, but not the second. The only way I see to handle this is to somehow communicate to the encodedString codec that the input stream is misaligned. But of course, scodec is pure functional and PlanetSide decoding is imperative (meaning the decoding class has access to the current position in the overall bitstream).

Scodec may need to be modified or something completely custom may have to be done for this case. I would prefer to have a general solution as this issue is definitely going to come up again in a different way.

Generate code documentation and diagrams.

In order for people to actually have a chance at contributing to this project, good documentation will be required. Scala is a hard enough barrier to entry as is. Bad documentation cannot be afforded.

Learn how to generate ScalaDocs: http://alvinalexander.com/scala/how-to-generate-scala-documentation-scaladoc-command-examples

Beyond ScalaDocs, higher level architectural diagrams will be required as this game server is Actor/Service oriented and will not be sharing memory between Actors. This is hard enough for me to figure out. I can't imagine the nightmare it would be for someone else new to this paradigm.

Non-unique actors are trying to be created

Not sure why this happens, but it occurs randomly when zoning. Seems like a race condition when switching zones.

player.Actor = context.actorOf(Props(classOf[PlayerControl], player), s"${player.Name}_${player.GUID.guid}")

2020-01-12 01:27:09,338  INFO "sessionId=10" WorldSessionActor - Load in zone z4 at position Vector3(3059.0,2144.0,70.0) in 0 seconds
2020-01-12 01:27:09,338  INFO "sessionId=10" WorldSessionActor - Chat: ChatMsg(CMT_ZONE,true,,z4,None)
2020-01-12 01:27:09,338  INFO "" WorldSessionActor - Player asdf will respawn
2020-01-12 01:27:09,338  INFO "" WorldSessionActor - Setting up combat engineering UI ...
2020-01-12 01:27:09,339 ERROR "sourceThread=PsLogin-akka.actor.default-dispatcher-12, akkaSource=akka://PsLogin/user/service/cluster/z4-actor/z4-players, sourceActorSystem=PsLogin, akkaTimestamp=01:27:09.339UTC" akka.actor.OneForOneStrategy - actor name [asdf_3317] is not unique!
akka.actor.InvalidActorNameException: actor name [asdf_3317] is not unique!
	at akka.actor.dungeon.ChildrenContainer$NormalChildrenContainer.reserve(ChildrenContainer.scala:129)
	at akka.actor.dungeon.Children$class.reserveChild(Children.scala:130)
	at akka.actor.ActorCell.reserveChild(ActorCell.scala:374)
	at akka.actor.dungeon.Children$class.makeChild(Children.scala:268)
	at akka.actor.dungeon.Children$class.actorOf(Children.scala:42)
	at akka.actor.ActorCell.actorOf(ActorCell.scala:374)
	at net.psforever.objects.zones.ZonePopulationActor$$anonfun$receive$1.applyOrElse(ZonePopulationActor.scala:48)
	at akka.actor.Actor$class.aroundReceive(Actor.scala:482)
	at net.psforever.objects.zones.ZonePopulationActor.aroundReceive(ZonePopulationActor.scala:21)
	at akka.actor.ActorCell.receiveMessage(ActorCell.scala:526)
	at akka.actor.ActorCell.invoke(ActorCell.scala:495)
	at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
	at akka.dispatch.Mailbox.run(Mailbox.scala:224)
	at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
	at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
	at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
	at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
	at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
2020-01-12 01:27:09,750  INFO "sessionId=10" WorldSessionActor - CreateShortcutMessage: CreateShortcutMessage(ValidPlanetSideGUID(0),1,0,true,Some(Shortcut(0,medkit,,)))
2020-01-12 01:27:09,750  INFO "sessionId=10" WorldSessionActor - AvatarFirstTimeEvent: AvatarFirstTimeEventMessage(ValidPlanetSideGUID(3317),ValidPlanetSideGUID(6864),1,used_pulsar)
``

Make packet descriptions more DRY?

Currently there is a lot of boilerplate required to describe a new packet (names used in multiple places) and a lot of cross linking required between decoding and encoding. Maybe there isn't a way to avoid this but at the very least a way to template this would be awesome.

Thoughts below.

Crash - Combat Engineering - Damaging a deployable cadaver crashes WSA

2020-01-12 00:12:59,448  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(355,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40113),Vector3(982.21094,1023.71094,100.875),0,446,63536,200,255,0,None)
2020-01-12 00:12:59,448  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(356,ValidPlanetSideGUID(40113),0,Some(HitInfo(Vector3(982.21094,1023.71094,100.875),Vector3(983.7422,1022.2344,100.515625),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:12:59,606  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(360,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40114),Vector3(982.2031,1023.71875,100.890625),0,486,63546,200,255,0,None)
2020-01-12 00:12:59,606  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(360,ValidPlanetSideGUID(40114),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.75,1022.2422,100.5),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:12:59,777  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(365,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40115),Vector3(982.2031,1023.71875,100.890625),0,462,63549,200,255,0,None)
2020-01-12 00:12:59,777  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(365,ValidPlanetSideGUID(40115),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.75,1022.2422,100.515625),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:12:59,886  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(370,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40116),Vector3(982.2031,1023.71875,100.890625),0,484,63574,200,255,0,None)
2020-01-12 00:12:59,886  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(370,ValidPlanetSideGUID(40116),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.7656,1022.2578,100.5),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:00,108  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(375,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40117),Vector3(982.2031,1023.71875,100.890625),0,480,63569,200,255,0,None)
2020-01-12 00:13:00,108  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(375,ValidPlanetSideGUID(40117),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.7578,1022.2578,100.5),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:00,262  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(380,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40118),Vector3(982.2031,1023.71875,100.890625),0,495,63594,200,255,0,None)
2020-01-12 00:13:00,262  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(380,ValidPlanetSideGUID(40118),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.78125,1022.2656,100.484375),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:00,404  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(384,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40119),Vector3(982.2031,1023.71875,100.890625),0,495,63574,200,255,0,None)
2020-01-12 00:13:00,404  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(385,ValidPlanetSideGUID(40119),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.7656,1022.2578,100.484375),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:00,581  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(389,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40120),Vector3(982.2031,1023.71875,100.890625),0,489,63583,200,255,0,None)
2020-01-12 00:13:00,581  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(389,ValidPlanetSideGUID(40120),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.77344,1022.2578,100.484375),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:00,756  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(394,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40121),Vector3(982.2031,1023.71875,100.890625),0,497,63567,200,255,0,None)
2020-01-12 00:13:00,757  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(394,ValidPlanetSideGUID(40121),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.7578,1022.25,100.484375),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:00,911  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(399,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40122),Vector3(982.2031,1023.71875,100.890625),0,483,63568,200,255,0,None)
2020-01-12 00:13:00,911  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(399,ValidPlanetSideGUID(40122),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.7578,1022.2578,100.5),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:01,073  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(404,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40123),Vector3(982.2031,1023.71875,100.890625),0,490,63598,200,255,0,None)
2020-01-12 00:13:01,073  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(404,ValidPlanetSideGUID(40123),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.78906,1022.2656,100.484375),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:01,151  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(409,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40124),Vector3(982.2031,1023.71875,100.890625),0,489,63560,200,255,0,None)
2020-01-12 00:13:01,248  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(409,ValidPlanetSideGUID(40124),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.7578,1022.25,100.484375),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:01,249 DEBUG "" services.support.SupportActor - a target submitted: Entry(net.psforever.objects.SensorDeployable@13e8b19d,Zones$$anon$14@12bb4822,0)
2020-01-12 00:13:01,267 DEBUG "" services.support.SupportActor - no tasks matching the targets List(net.psforever.objects.SensorDeployable@13e8b19d) have been cleared
2020-01-12 00:13:01,371  INFO "sessionId=7" WorldSessionActor - WeaponFire: WeaponFireMessage(414,ValidPlanetSideGUID(9829),ValidPlanetSideGUID(40100),Vector3(982.2031,1023.71875,100.890625),0,481,63550,200,255,0,None)
2020-01-12 00:13:01,371  INFO "sessionId=7" WorldSessionActor - Hit: HitMessage(414,ValidPlanetSideGUID(40100),0,Some(HitInfo(Vector3(982.2031,1023.71875,100.890625),Vector3(983.75,1022.2422,100.5),Some(ValidPlanetSideGUID(7460)))),true,false,None)
2020-01-12 00:13:01,376 ERROR "sourceThread=PsLogin-akka.actor.default-dispatcher-14, akkaSource=akka://PsLogin/user/world-udp-endpoint/world-session-router/world-session-7, sourceActorSystem=PsLogin, akkaTimestamp=00:13:01.374UTC" akka.actor.OneForOneStrategy - null
java.lang.NullPointerException: null
	at WorldSessionActor.HandleDealingDamage(WorldSessionActor.scala:8712)
	at WorldSessionActor.handleGamePkt(WorldSessionActor.scala:5661)
	at WorldSessionActor$$anonfun$Started$1.applyOrElse(WorldSessionActor.scala:359)
	at scala.PartialFunction$OrElse.applyOrElse(PartialFunction.scala:171)
	at akka.actor.Actor$class.aroundReceive(Actor.scala:482)
	at WorldSessionActor.akka$actor$MDCContextAware$$super$aroundReceive(WorldSessionActor.scala:80)
	at akka.actor.MDCContextAware$class.aroundReceive(MDCContextAware.scala:23)
	at WorldSessionActor.aroundReceive(WorldSessionActor.scala:80)
	at akka.actor.ActorCell.receiveMessage(ActorCell.scala:526)
	at akka.actor.ActorCell.invoke(ActorCell.scala:495)
	at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
	at akka.dispatch.Mailbox.run(Mailbox.scala:224)
	at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
	at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
	at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
	at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
	```

Unhandled Cryptopacket Bitvector

Resulted in an "Unknown Login Error" message during Login Progress.
image

See debug log: pslogin-debug_2019-10-11_20-45-09.log

[ERROR] CryptoSessionActor - Could not decode packet in state CryptoSetupFinishing: unmarshal_crypto_packet/obj_type?: expected constant BitVector(8 bits, 0x10) but got BitVector(8 bits, 0x01) [ERROR] CryptoSessionActor - Could not decode packet in state CryptoSetupFinishing: unmarshal_crypto_packet/obj_type?: expected constant BitVector(8 bits, 0x10) but got BitVector(8 bits, 0x01) [ERROR] CryptoSessionActor - Could not decode packet in state CryptoSetupFinishing: unmarshal_crypto_packet/obj_type?: expected constant BitVector(8 bits, 0x10) but got BitVector(8 bits, 0x01)

Complete abstraction for PlanetSide message channels

PlanetSide runs on top of UDP. UDP is not reliable.

We want a nice way for the user to send messages to a client with some reliability guarantees without worrying about how to construct a SlottedMetaPacket. We should completely abstract away from fragmentation issues, packet reordering, and packet loss.

Code needs to be written to correctly these cases if we are going to have a reliable server.
This is related to #3

Fix GamePacket vs GamePacketContainer naming and datastructures

I cannot for the life of me, even as the author, reliably write code that deals with unwrapping packet containers in to inner packets. The naming scheme must change.

Also, the decision to handle packet sequence numbers in the containers was not well thought out as more reverse engineering had to be done in order to figure out how they were actually being used. Now I know once encryption is enabled that they are used for game and control packets. Control packet containers do not have a field for sequence numbers. This is because the first packets that are sent are control packets, but they are NOT sequences.

This maybe should be rethought as it is just not working as is.

Gating - Warpgates - Various Issues with Warpgates

TR can only get to Solsar from Sanctuary, and capturing Solsar does not grant you additional links.
NC has no functioning Warpgates, they all "one-way" instantly warp you back to Sanctuary Spawn
VS can only get to Ceryshen from Sanctuary

I think the current Test Live (51200) does something special to handle this. @SouNourS would know.

Refactor WorldSessionActor

It's been said for a while at this point, but WorldSessionActor needs to be broken up into smaller files. It has grown to nearly 10K lines of Scala, which is dense as it is. Maintaining and understanding this file is likely next to impossible for new developers. It's so big that TravisCI fails to compile it with code coverage enabled!

Nearly all of the player business logic for PlanetSide is in this single file. Let's start to logically divide this up into smaller sub-files.

LoginSessionActor dies upon bad match

SessionRouter or some session specific parent needs to watch child Actors for death events and appropriately restart them.

2016-05-01 23:28:40,326 ERROR [PsLogin-akka.actor.default-dispatcher-10 sessionId=none] akka.actor.OneForOneStrategy - MultiPacket(Vector(ByteVector(62 bytes, 0x0009000403895053466f72657665720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000), ByteVector(62 bytes, 0x0009000203895053466f72657665720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000))) (of class psforever.net.MultiPacket)
scala.MatchError: MultiPacket(Vector(ByteVector(62 bytes, 0x0009000403895053466f72657665720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000), ByteVector(62 bytes, 0x0009000203895053466f72657665720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000))) (of class psforever.net.MultiPacket)
        at LoginSessionActor.handleControlPkt(LoginSessionActor.scala:37)
        at LoginSessionActor$$anonfun$Started$1.applyOrElse(LoginSessionActor.scala:30)
        at akka.actor.Actor$class.aroundReceive(Actor.scala:482)
        at LoginSessionActor.akka$actor$MDCContextAware$$super$aroundReceive(LoginSessionActor.scala:9)
        at akka.actor.MDCContextAware$class.aroundReceive(MDCContextAware.scala:25)
        at LoginSessionActor.aroundReceive(LoginSessionActor.scala:9)
        at akka.actor.ActorCell.receiveMessage(ActorCell.scala:526)
        at akka.actor.ActorCell.invoke(ActorCell.scala:495)
        at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
        at akka.dispatch.Mailbox.run(Mailbox.scala:224)
        at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
        at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
        at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
        at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
        at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
2016-05-01 23:28:41,148 ERROR [PsLogin-akka.actor.default-dispatcher-31 sessionId=2] CryptoSessionActor - Failed to decode encrypted packet: Failed to parse control packet 0x09: unknown: expected constant BitVector(8 bits, 0x00) but got BitVector(8 bits, 0x03)
2016-05-01 23:28:42,041 ERROR [PsLogin-akka.actor.default-dispatcher-30 sessionId=2] CryptoSessionActor - Failed to decode encrypted packet: Failed to parse control packet 0x09: unknown: expected constant BitVector(8 bits, 0x00) but got BitVector(8 bits, 0x01)
2016-05-01 23:28:42,773 ERROR [PsLogin-akka.actor.default-dispatcher-31 sessionId=none] LoginSessionActor - Unknown message
2016-05-01 23:28:43,743 ERROR [PsLogin-akka.actor.default-dispatcher-31 sessionId=2] CryptoSessionActor - Failed to decode encrypted packet: Failed to parse control packet 0x09: unknown: expected constant BitVector(8 bits, 0x00) but got BitVector(8 bits, 0x04)
2016-05-01 23:28:43,885 ERROR [PsLogin-akka.actor.default-dispatcher-33 sessionId=2] CryptoSessionActor - Failed to decode encrypted packet: Failed to parse control packet 0x09: unknown: expected constant BitVector(8 bits, 0x00) but got BitVector(8 bits, 0x02)
2016-05-01 23:28:44,837 ERROR [PsLogin-akka.actor.default-dispatcher-33 sessionId=2] CryptoSessionActor - Failed to decode encrypted packet: Failed to parse control packet 0x09: unknown: expected constant BitVector(8 bits, 0x00) but got BitVector(8 bits, 0x03)
2016-05-01 23:28:45,851 ERROR [PsLogin-akka.actor.default-dispatcher-22 sessionId=2] CryptoSessionActor - Failed to decode encrypted packet: Failed to parse control packet 0x09: unknown: expected constant BitVector(8 bits, 0x00) but got BitVector(8 bits, 0x01)
2016-05-01 23:28:46,715 INFO  [PsLogin-akka.actor.default-dispatcher-33 sessionId=none] akka.actor.LocalActorRef - Message [psforever.net.ControlPacket] from Actor[akka://PsLogin/user/session-router/crypto-session2#1836272772] to Actor[akka://PsLogin/user/session-router/login-session2#-143057463] was not delivered. [1] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.

Session dropping on world transfer has race condition

When sending the below

 val response = ConnectToWorldMessage(serverName, serverAddress.getHostString, serverAddress.getPort)
sendResponse(PacketCoding.CreateGamePacket(0, response))
sendResponse(DropSession(sessionId, "user transferring to world"))

combined with this

 def removeSessionById(id : Long, reason : String, graceful : Boolean) : Unit = {
    ...
    if(graceful) {
      for(i <- 0 to 5) {
        session.send(closePacket)
      }
    }

    // kill all session specific actors
    session.dropSession(graceful)
    ...
}

Can cause the client to fail when connecting to the world. Either we dont do this gracefully or we add a delay

2016-07-31 18:50:59,704  INFO "" login-session-router - New session ID=45 from /X:54346
2016-07-31 18:51:00,164  INFO "sessionId=45" LoginSessionActor - New login UN:ASDF PW:Some(ASDF). Client Version: 3.15.84, Dec  2 2009
2016-07-31 18:51:00,401  INFO "" world-session-router - New session ID=64 from /X:54345
2016-07-31 18:51:01,482  INFO "sessionId=45" LoginSessionActor - New login UN:ASDF PW:Some(ASDF). Client Version: 3.15.84, Dec  2 2009
2016-07-31 18:51:01,482  INFO "sessionId=45" LoginSessionActor - Connect to world request for 'PSForever'
2016-07-31 18:51:01,482  INFO "sessionId=45" login-session-router - Dropping session ID=45 (reason: user transferring to world)
2016-07-31 18:51:02,181  INFO "sourceThread=PsLogin-akka.actor.default-dispatcher-4, akkaSource=akka://PsLogin/user/login-udp-endpoint/login-session-router/login-session-45, sourceActorSystem=PsLogin, akkaTimestamp=16:51:02.179UTC" akka.actor.LocalActorRef - Message [LoginSessionActor$UpdateServerList] from Actor[akka://PsLogin/user/login-udp-endpoint/login-session-router/login-session-45#-1487728806] to Actor[akka://PsLogin/user/login-udp-endpoint/login-session-router/login-session-45#-1487728806] was not delivered. [1] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
2016-07-31 18:51:02,765 DEBUG "" login-session-router - Session(45, 954)
2016-07-31 18:51:02,765 DEBUG "" world-session-router - Session(64, 18)
2016-07-31 18:51:02,765 DEBUG "" login-session-router - Reaped session ID=45
2016-07-31 18:51:07,765 DEBUG "" world-session-router - Session(64, 36)
2016-07-31 18:51:10,104  INFO "" login-session-router - New session ID=46 from /X:54346
2016-07-31 18:51:10,105 ERROR "sessionId=46" CryptoSessionActor - Unexpected packet type EncryptedPacket(3,ByteVector(64 bytes, 0xf0603c68661fe8c7fb763576da764333fb763576da7643337d509584c0caadf043a470a8dbda3fbeb9f813e8de63c01d883a6e43af64c87e7d509584c0caadf0)) in state NewClient
2016-07-31 18:51:12,765 DEBUG "" world-session-router - Session(64, 54)
2016-07-31 18:51:12,765 DEBUG "" login-session-router - Session(46, 68)
2016-07-31 18:51:17,765 DEBUG "" login-session-router - Session(46, 68)
2016-07-31 18:51:17,765 DEBUG "" world-session-router - Session(64, 72)
2016-07-31 18:51:17,765  INFO "" login-session-router - Dropping session ID=46 (reason: session timed out (outbound))
2016-07-31 18:51:22,766 DEBUG "" world-session-router - Session(64, 90)
2016-07-31 18:51:22,766 DEBUG "" login-session-router - Session(46, 80)
2016-07-31 18:51:22,766 DEBUG "" login-session-router - Reaped session ID=46
2016-07-31 18:51:27,765 DEBUG "" world-session-router - Session(64, 108)
2016-07-31 18:51:30,135  INFO "" login-session-router - New session ID=47 from /X:54346
2016-07-31 18:51:30,135 ERROR "sessionId=47" CryptoSessionActor - Unexpected packet type EncryptedPacket(5,ByteVector(64 bytes, 0x68b300328436490dfb763576da764333fb763576da7643334cfbe2e844ef1969d6ddee09f3f196a72a05b601e766571e0f32b9dad9b89d9d7d509584c0caadf0)) in state NewClient

Docker build

It may be nice to have a Dockerfile so people can easily build the server, will probably make it easier for people to test too! I can have a stab at this over the next few days if you think it would be useful? :)

Change default file logger to rolling logger

Log files can get very big if the server is running for a while. Change config/logback.xml to incorporate rolling files
https://stackify.com/logging-logback/

<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>log-%d{yyyy-MM-dd}.log</fileNamePattern>
        <maxHistory>30</maxHistory> 
        <totalSizeCap>3GB</totalSizeCap>
    </rollingPolicy>
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
        <maxFileSize>3MB</maxFileSize>
    </triggeringPolicy>
    <encoder>
        <pattern>[%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

Choose database solution for the LoginServer

Some candidates:

  • MongoDB
  • CouchDB
  • MySQL
  • Flat file
  • ...

It seems like NoSQL / document based DB would allow me to move fast and use plain ol' JSON. A SQL based DB would probably be overkill unless that's preferred for account records. More transient data, such as game state may benefit more from a NoSQL solution.

Cannot dismount "owned" AMS after mounting

2019-12-15 21:27:01,901  INFO "sessionId=108" WorldSessionActor - DismountVehicleMsg: DismountVehicleMsg(PlanetSideGUID(5113),Normal,false)
2019-12-15 21:27:01,901  WARN "" WorldSessionActor - DismountVehicleMsg: TR awildchord i3-5113 100/100 50/50 attempted to dismount ams, owned by Some(PlanetSideGUID(5113)): (3000/3000)(0/601) (1)'s seat 0, but was not allowed

I was allowed to enter but not leave ๐Ÿ˜ฌ

HotSpotUpdateMessage: Byte-Aligned Lists

HotSpotUpdateMessage is the packet that creates those explosions on the continental map that indicate where combat took place recently. While I think I understand how the packet is composed, and could otherwise write it, there's a bit of a snag with the decoding/encoding. The packet stores hotspot data as a list preceded by its size - scodec otherwise knows how to handle that - but the size is padded from the actual list by four bits. This makes the contents of the list byte-aligned. Scodec doesn't know how to handle that with its standard function listOfN. While it does come with a stock sizedList function that can handle manually provided sizes, the size has to be a non-negative Integer literal. if I could get there from a variable gotten via >>:~ from the same decoding process, that would be good, but I don't know how to do that. The function is not convinced.

I would like a function that works like listOfN, excepting that it ignores padding, but I can not repurpose the existing methods myself. listOfN handles List extraction by passing the buck off to a function called narrow; narrow does the same to exmap; and, the result of this chain is a Tuple that contains both the size and the already-extracted List data. The whole process is a bit spooky to walk through. The approach as with aligned wide-character Strings (issue #67) also might not be possible here.

Here's a sample: first a packet with no entries, a packet with one entry, then that same packets with the nibbles spaced by field.
9F 05 00 10 00
9F 05 00 10 10 00 2E 90 01 45 80 00 00
9F 0500 1 00 (0)
9F 0500 1 01000 2E9 00 145 80000 (0)
That weird 0 is the four bits of padding, with the size of the list as the byte before it and everything after it a part of the entry. The padding also can not be made a part of the entry to eliminate it because each entry begins right after the previous, with the first byte starting in place of the former trailing nibble.
9F 05 00 10 20 00 D0 70 08 CA 80 00 00 0B EA 00 4C 48 00 00
9F 0500 1 02000D07008CA80000 00BEA004C480000

Unbreak scoverage on WorldSessionActor

Requires #278 to have been partially addressed to lower the JVM bytecode size. There is no fix besides refactoring this file.

[error] /home/travis/build/psforever/PSF-LoginServer/pslogin/src/main/scala/WorldSessionActor.scala:67: Could not write class WorldSessionActor because it exceeds JVM code size limits. Method handleGamePkt's code too large!
[error] class WorldSessionActor extends Actor with MDCContextAware {
[error]       ^
[error] one error found
[error] (pslogin/compile:compileIncremental) Compilation failed
[error] Total time: 434 s, completed Oct 11, 2019 3:38:47 PM

Here is some context: https://github.com/scoverage/sbt-scoverage/issues/92

Make Control Packets more Approachable

Comment extracted from PR #152 :

"[In regards to working with SlottedMetaPackets,] after reviewing how packets are handled, I determined the best place to intercept packets as they left the server was CyptoSessionActor. They leave WorldSessionActor to CSA as packets and they leave CSA to SessionRouter as bit/byte vectors. If I were to handle them at the WSA level, I'd be testing the packet length by prematurely encoding them and, if it was a sufficient length for maximum transmission unit restrictions, I'd be stuck. Since I wouldn't be manipulating them any further, either I would have to send out the original packet and have it encoded a second time later or I would send out my encoded data and completely bypass checks that were written into CSA. Hoisting [this logic] out of CSA means that the same complicated procedures would have to be repeated in LoginSessionActor as well as anything else we build on top of CSA.
"...
"SlottedMetaPacket and MultiPacket/MultiPacketEx need to change. That's my opinion. The data that is supposed to be put in them is a 'stream', but that inconveniences our ability to construct them since a 'stream' is further along in our process of encoding than we allow input and control. We lack the ability of feeding multiple packets into Multi, for example, because our normal procedure works one packet at a time. It's unnecessarily restricting and suggests the doubling of work. We extract and decode the data from these Control packets whenever we need it, and just once, because, until we process the packet, it's just a Control packet with a 'stream of data.'"

Fix Travis CI build

Travis CI is broken because we don't have a way to get the pscrypto library in to the build environment.

This isn't just a problem for Travis -- no one will be able to develop without this library.

Correctly handle SlottedMetaPacket

The current implementation is a hack. These packets are treated specially in the PS client (they are "slots" for reliable subpackets). The client only has 8 slots and the way it selects between them is strange.

Additionally, the server does not handle this packet type at all. The client will disconnect without this being handled.

Crash - CTD - Zone Transfer - When characters zone, the client often crashes to desktop during the Loading Screen

When characters zone, the client often crashes to desktop during the Loading Screen.

Current Result: The game client will commonly crash to desktop during zone transfer.
Expected Result: The game client should never crash during zone transfer.

Reproduction Steps:

  • Using the /zone command, transfer between different continents repeatedly (you can spam /zone z2 /zone z3 /zone z2 /zone z3 etc)
  • Observe crash to desktop

PlanetSide Combat Engineering

Currently we have no support for placed or triggered CE. This was a core aspect of planetside gameplay

  • Spitfires (server AI, aimbotting, hit scan)
  • Mines (enemy detection, blast radius)
  • Boomers (blast radius effect needed)
  • Motion Sensor Alarms (detection radius)
  • TRAP
  • AEGIS
    • Shielding (passive)
    • Terminal
  • Faction Turret

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.