Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Java CI/CD

on:
push:
tags:
- '*'
pull_request:
branches:
- '**'

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
cache: maven
- name: Set version
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "REVISION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
else
echo "REVISION=0.0.0-SNAPSHOT" >> $GITHUB_ENV
fi
- name: Build with Maven
run: mvn -B package -Drevision=${{ env.REVISION }} --file pom.xml

publish:
if: startsWith(github.ref, 'refs/tags/')
needs: build-and-test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
server-id: github
settings-path: ${{ github.workspace }}
- name: Publish to GitHub Packages
run: mvn -B deploy -DskipTests -Drevision=${{ github.ref_name }} --file pom.xml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 changes: 4 additions & 23 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
# Compiled class file
*.class
# IDE
/.idea/

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
# Build
/target/
File renamed without changes.
129 changes: 128 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,129 @@
# a2s-java
Valve Steam Query Protocol implementation for Java

A Valve Steam Query Protocol (A2S) implementation for Java using Netty.

This library allows you to query game servers that implement the Source Engine Query protocol, such as Counter-Strike, Team Fortress 2, Rust, ARK, and many others.

> [!NOTE]
> This code is based on and used from [yeetus-desastroesus/A2S-Java](https://github.com/yeetus-desastroesus/A2S-Java).

## Features

- **A2S_INFO**: Get server information (name, map, player count, etc.).
- **A2S_PLAYER**: Get detailed list of players currently on the server.
- **A2S_RULES**: Get server rules/CVars.
- **Asynchronous**: Built on Netty for high-performance, non-blocking I/O.
- **Server Implementation**: Includes a basic A2S server implementation for testing or mocking.

## Installation

The library is published on GitHub Packages. To use it, you need to configure your build tool to include the GitHub Maven repository.

### Maven

1. Add the following repository to your `pom.xml`:

```xml
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/g-portal/a2s-java</url>
</repository>
</repositories>
```

2. Add the following dependency to your `pom.xml`:

```xml
<dependency>
<groupId>com.gportal</groupId>
<artifactId>a2s</artifactId>
<version>1.0.0</version> <!-- Use the desired release tag version -->
</dependency>
```

### Gradle

1. Add the following repository to your `build.gradle`:

```gradle
repositories {
maven {
url = uri("https://maven.pkg.github.com/g-portal/a2s-java")
}
}
```

2. Add the following to your `build.gradle` dependencies:

```gradle
dependencies {
implementation 'com.gportal:a2s:1.0.0' // Use the desired release tag version
}
```

## Usage

### Querying a Server (Client)

```java
import com.gportal.source.query.QueryClient;
import com.gportal.source.query.ServerInfo;
import com.gportal.source.query.PlayerInfo;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

public class Example {
public static void main(String[] args) throws Exception {
QueryClient client = new QueryClient();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 27015);

// Query Server Info
CompletableFuture<ServerInfo> infoFuture = client.queryServer(address);
infoFuture.thenAccept(info -> System.out.println("Server Name: " + info.name()));

// Query Players
CompletableFuture<List<PlayerInfo>> playersFuture = client.queryPlayers(address);
playersFuture.thenAccept(players -> players.forEach(p -> System.out.println("Player: " + p.name())));

// Query Rules
CompletableFuture<Map<String, String>> rulesFuture = client.queryRules(address);
rulesFuture.thenAccept(rules -> System.out.println("Rules count: " + rules.size()));

// Don't forget to shutdown when done
// client.shutdown();
}
}
```

### Starting an A2S Server

```java
import com.gportal.source.query.QueryServer;
import com.gportal.source.query.ServerInfo;
import com.gportal.source.query.PlayerInfo;

public class ServerExample {
public static void main(String[] args) {
ServerInfo info = new ServerInfo(
null, (byte)17, "My Java Game Server", "de_dust2", "csgo", "Counter-Strike: Global Offensive",
(short)730, (byte)0, (byte)20, (byte)0, 'd', 'l', false, true, "1.0.0.0",
null, null, null, null, null, null
);

QueryServer server = new QueryServer(27015, info);

// Add some players or rules
server.players.add(new PlayerInfo((byte)0, "Gordon Freeman", (short)100, 300.0f));
server.rules.put("mp_timelimit", "30");

System.out.println("A2S Server started on port 27015");
}
}
```

## License

This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
39 changes: 39 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.gportal</groupId>
<artifactId>a2s</artifactId>
<version>${revision}</version>

<name>a2s-query</name>

<properties>
<revision>1.0.0-SNAPSHOT</revision>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<distributionManagement>
<repository>
<id>github</id>
<name>GitHub Packages</name>
<url>https://maven.pkg.github.com/g-portal/a2s-java</url>
</repository>
</distributionManagement>

<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.2.9.Final</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
21 changes: 21 additions & 0 deletions src/main/java/com/gportal/source/query/Message.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.gportal.source.query;

import java.net.InetSocketAddress;

import io.netty.buffer.ByteBuf;

public interface Message {
public InetSocketAddress remoteAddress();
public Message write(ByteBuf buffer);

public static String readString(ByteBuf buffer) {
String val = "";
byte in;
while((in = buffer.readByte()) != 0) val += (char) in;
return val;
}
public static void writeString(ByteBuf buffer, String val) {
buffer.writeBytes(val.getBytes());
buffer.writeByte(0);
}
}
40 changes: 40 additions & 0 deletions src/main/java/com/gportal/source/query/MessageCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.gportal.source.query;

import java.util.List;

import com.gportal.source.query.messages.ChallengeReply;
import com.gportal.source.query.messages.InfoQuery;
import com.gportal.source.query.messages.InfoReply;
import com.gportal.source.query.messages.PlayerQuery;
import com.gportal.source.query.messages.PlayerReply;
import com.gportal.source.query.messages.RulesQuery;
import com.gportal.source.query.messages.RulesReply;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageCodec;

public class MessageCodec extends MessageToMessageCodec<DatagramPacket, Message> {
protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List<Object> out) throws Exception {
int header = msg.content().readIntLE();
if(header != -1) throw new UnsupportedOperationException("we dont support split packets yet");
byte op = msg.content().readByte();
switch(op) {
case ChallengeReply.OP: out.add(ChallengeReply.read(msg.sender(), msg.content())); break;
case InfoQuery.OP: out.add(InfoQuery.read(msg.sender(), msg.content())); break;
case InfoReply.OP: out.add(InfoReply.read(msg.sender(), msg.content())); break;
case PlayerQuery.OP: out.add(PlayerQuery.read(msg.sender(), msg.content())); break;
case PlayerReply.OP: out.add(PlayerReply.read(msg.sender(), msg.content())); break;
case RulesQuery.OP: out.add(RulesQuery.read(msg.sender(), msg.content())); break;
case RulesReply.OP: out.add(RulesReply.read(msg.sender(), msg.content())); break;
default: throw new UnsupportedOperationException("Unknown OP 0x" + String.format("%2x", op).replaceAll(" ", "0"));
}
}
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> out) throws Exception {
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeIntLE(-1);
msg.write(buffer);
out.add(new DatagramPacket(buffer, msg.remoteAddress()));
}
}
25 changes: 25 additions & 0 deletions src/main/java/com/gportal/source/query/PlayerInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.gportal.source.query;

import static com.gportal.source.query.Message.readString;
import static com.gportal.source.query.Message.writeString;

import io.netty.buffer.ByteBuf;

public record PlayerInfo(byte index, String name, int score, float duration) {
public static PlayerInfo read(ByteBuf buffer) {
byte index = buffer.readByte();
String name = readString(buffer);
int score = buffer.readIntLE();
float duration = buffer.readFloatLE();

return new PlayerInfo(index, name, score, duration);
}
public PlayerInfo write(ByteBuf buffer) {
buffer.writeByte(index());
writeString(buffer, name());
buffer.writeIntLE(score());
buffer.writeFloatLE(duration());

return this;
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/gportal/source/query/Query.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.gportal.source.query;

public interface Query extends Message {
public Integer challenge();
public Query withChallenge(int challenge);
}
Loading