Skip to content

Commit f794f9f

Browse files
authored
Merge pull request #1 from G-PORTAL/ci/cd
Added initial release
2 parents 4e8def5 + f53c115 commit f794f9f

22 files changed

Lines changed: 834 additions & 24 deletions

.github/workflows/ci-cd.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Java CI/CD
2+
3+
on:
4+
push:
5+
tags:
6+
- '*'
7+
pull_request:
8+
branches:
9+
- '**'
10+
11+
jobs:
12+
build-and-test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up JDK 25
17+
uses: actions/setup-java@v4
18+
with:
19+
java-version: '25'
20+
distribution: 'temurin'
21+
cache: maven
22+
- name: Set version
23+
run: |
24+
if [[ $GITHUB_REF == refs/tags/* ]]; then
25+
echo "REVISION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
26+
else
27+
echo "REVISION=0.0.0-SNAPSHOT" >> $GITHUB_ENV
28+
fi
29+
- name: Build with Maven
30+
run: mvn -B package -Drevision=${{ env.REVISION }} --file pom.xml
31+
32+
publish:
33+
if: startsWith(github.ref, 'refs/tags/')
34+
needs: build-and-test
35+
runs-on: ubuntu-latest
36+
permissions:
37+
contents: read
38+
packages: write
39+
steps:
40+
- uses: actions/checkout@v4
41+
- name: Set up JDK 25
42+
uses: actions/setup-java@v4
43+
with:
44+
java-version: '25'
45+
distribution: 'temurin'
46+
server-id: github
47+
settings-path: ${{ github.workspace }}
48+
- name: Publish to GitHub Packages
49+
run: mvn -B deploy -DskipTests -Drevision=${{ github.ref_name }} --file pom.xml
50+
env:
51+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,5 @@
1-
# Compiled class file
2-
*.class
1+
# IDE
2+
/.idea/
33

4-
# Log file
5-
*.log
6-
7-
# BlueJ files
8-
*.ctxt
9-
10-
# Mobile Tools for Java (J2ME)
11-
.mtj.tmp/
12-
13-
# Package Files #
14-
*.jar
15-
*.war
16-
*.nar
17-
*.ear
18-
*.zip
19-
*.tar.gz
20-
*.rar
21-
22-
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23-
hs_err_pid*
24-
replay_pid*
4+
# Build
5+
/target/
File renamed without changes.

README.md

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,129 @@
11
# a2s-java
2-
Valve Steam Query Protocol implementation for Java
2+
3+
A Valve Steam Query Protocol (A2S) implementation for Java using Netty.
4+
5+
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.
6+
7+
> [!NOTE]
8+
> This code is based on and used from [yeetus-desastroesus/A2S-Java](https://github.com/yeetus-desastroesus/A2S-Java).
9+
10+
## Features
11+
12+
- **A2S_INFO**: Get server information (name, map, player count, etc.).
13+
- **A2S_PLAYER**: Get detailed list of players currently on the server.
14+
- **A2S_RULES**: Get server rules/CVars.
15+
- **Asynchronous**: Built on Netty for high-performance, non-blocking I/O.
16+
- **Server Implementation**: Includes a basic A2S server implementation for testing or mocking.
17+
18+
## Installation
19+
20+
The library is published on GitHub Packages. To use it, you need to configure your build tool to include the GitHub Maven repository.
21+
22+
### Maven
23+
24+
1. Add the following repository to your `pom.xml`:
25+
26+
```xml
27+
<repositories>
28+
<repository>
29+
<id>github</id>
30+
<url>https://maven.pkg.github.com/g-portal/a2s-java</url>
31+
</repository>
32+
</repositories>
33+
```
34+
35+
2. Add the following dependency to your `pom.xml`:
36+
37+
```xml
38+
<dependency>
39+
<groupId>com.gportal</groupId>
40+
<artifactId>a2s</artifactId>
41+
<version>1.0.0</version> <!-- Use the desired release tag version -->
42+
</dependency>
43+
```
44+
45+
### Gradle
46+
47+
1. Add the following repository to your `build.gradle`:
48+
49+
```gradle
50+
repositories {
51+
maven {
52+
url = uri("https://maven.pkg.github.com/g-portal/a2s-java")
53+
}
54+
}
55+
```
56+
57+
2. Add the following to your `build.gradle` dependencies:
58+
59+
```gradle
60+
dependencies {
61+
implementation 'com.gportal:a2s:1.0.0' // Use the desired release tag version
62+
}
63+
```
64+
65+
## Usage
66+
67+
### Querying a Server (Client)
68+
69+
```java
70+
import com.gportal.source.query.QueryClient;
71+
import com.gportal.source.query.ServerInfo;
72+
import com.gportal.source.query.PlayerInfo;
73+
import java.net.InetSocketAddress;
74+
import java.util.List;
75+
import java.util.Map;
76+
import java.util.concurrent.CompletableFuture;
77+
78+
public class Example {
79+
public static void main(String[] args) throws Exception {
80+
QueryClient client = new QueryClient();
81+
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 27015);
82+
83+
// Query Server Info
84+
CompletableFuture<ServerInfo> infoFuture = client.queryServer(address);
85+
infoFuture.thenAccept(info -> System.out.println("Server Name: " + info.name()));
86+
87+
// Query Players
88+
CompletableFuture<List<PlayerInfo>> playersFuture = client.queryPlayers(address);
89+
playersFuture.thenAccept(players -> players.forEach(p -> System.out.println("Player: " + p.name())));
90+
91+
// Query Rules
92+
CompletableFuture<Map<String, String>> rulesFuture = client.queryRules(address);
93+
rulesFuture.thenAccept(rules -> System.out.println("Rules count: " + rules.size()));
94+
95+
// Don't forget to shutdown when done
96+
// client.shutdown();
97+
}
98+
}
99+
```
100+
101+
### Starting an A2S Server
102+
103+
```java
104+
import com.gportal.source.query.QueryServer;
105+
import com.gportal.source.query.ServerInfo;
106+
import com.gportal.source.query.PlayerInfo;
107+
108+
public class ServerExample {
109+
public static void main(String[] args) {
110+
ServerInfo info = new ServerInfo(
111+
null, (byte)17, "My Java Game Server", "de_dust2", "csgo", "Counter-Strike: Global Offensive",
112+
(short)730, (byte)0, (byte)20, (byte)0, 'd', 'l', false, true, "1.0.0.0",
113+
null, null, null, null, null, null
114+
);
115+
116+
QueryServer server = new QueryServer(27015, info);
117+
118+
// Add some players or rules
119+
server.players.add(new PlayerInfo((byte)0, "Gordon Freeman", (short)100, 300.0f));
120+
server.rules.put("mp_timelimit", "30");
121+
122+
System.out.println("A2S Server started on port 27015");
123+
}
124+
}
125+
```
126+
127+
## License
128+
129+
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.

pom.xml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<groupId>com.gportal</groupId>
6+
<artifactId>a2s</artifactId>
7+
<version>${revision}</version>
8+
9+
<name>a2s-query</name>
10+
11+
<properties>
12+
<revision>1.0.0-SNAPSHOT</revision>
13+
<maven.compiler.source>25</maven.compiler.source>
14+
<maven.compiler.target>25</maven.compiler.target>
15+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16+
</properties>
17+
18+
<distributionManagement>
19+
<repository>
20+
<id>github</id>
21+
<name>GitHub Packages</name>
22+
<url>https://maven.pkg.github.com/g-portal/a2s-java</url>
23+
</repository>
24+
</distributionManagement>
25+
26+
<dependencies>
27+
<dependency>
28+
<groupId>io.netty</groupId>
29+
<artifactId>netty-all</artifactId>
30+
<version>4.2.9.Final</version>
31+
</dependency>
32+
<dependency>
33+
<groupId>org.junit.jupiter</groupId>
34+
<artifactId>junit-jupiter-api</artifactId>
35+
<version>6.0.2</version>
36+
<scope>test</scope>
37+
</dependency>
38+
</dependencies>
39+
</project>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.gportal.source.query;
2+
3+
import java.net.InetSocketAddress;
4+
5+
import io.netty.buffer.ByteBuf;
6+
7+
public interface Message {
8+
public InetSocketAddress remoteAddress();
9+
public Message write(ByteBuf buffer);
10+
11+
public static String readString(ByteBuf buffer) {
12+
String val = "";
13+
byte in;
14+
while((in = buffer.readByte()) != 0) val += (char) in;
15+
return val;
16+
}
17+
public static void writeString(ByteBuf buffer, String val) {
18+
buffer.writeBytes(val.getBytes());
19+
buffer.writeByte(0);
20+
}
21+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.gportal.source.query;
2+
3+
import java.util.List;
4+
5+
import com.gportal.source.query.messages.ChallengeReply;
6+
import com.gportal.source.query.messages.InfoQuery;
7+
import com.gportal.source.query.messages.InfoReply;
8+
import com.gportal.source.query.messages.PlayerQuery;
9+
import com.gportal.source.query.messages.PlayerReply;
10+
import com.gportal.source.query.messages.RulesQuery;
11+
import com.gportal.source.query.messages.RulesReply;
12+
13+
import io.netty.buffer.ByteBuf;
14+
import io.netty.channel.ChannelHandlerContext;
15+
import io.netty.channel.socket.DatagramPacket;
16+
import io.netty.handler.codec.MessageToMessageCodec;
17+
18+
public class MessageCodec extends MessageToMessageCodec<DatagramPacket, Message> {
19+
protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List<Object> out) throws Exception {
20+
int header = msg.content().readIntLE();
21+
if(header != -1) throw new UnsupportedOperationException("we dont support split packets yet");
22+
byte op = msg.content().readByte();
23+
switch(op) {
24+
case ChallengeReply.OP: out.add(ChallengeReply.read(msg.sender(), msg.content())); break;
25+
case InfoQuery.OP: out.add(InfoQuery.read(msg.sender(), msg.content())); break;
26+
case InfoReply.OP: out.add(InfoReply.read(msg.sender(), msg.content())); break;
27+
case PlayerQuery.OP: out.add(PlayerQuery.read(msg.sender(), msg.content())); break;
28+
case PlayerReply.OP: out.add(PlayerReply.read(msg.sender(), msg.content())); break;
29+
case RulesQuery.OP: out.add(RulesQuery.read(msg.sender(), msg.content())); break;
30+
case RulesReply.OP: out.add(RulesReply.read(msg.sender(), msg.content())); break;
31+
default: throw new UnsupportedOperationException("Unknown OP 0x" + String.format("%2x", op).replaceAll(" ", "0"));
32+
}
33+
}
34+
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> out) throws Exception {
35+
ByteBuf buffer = ctx.alloc().buffer();
36+
buffer.writeIntLE(-1);
37+
msg.write(buffer);
38+
out.add(new DatagramPacket(buffer, msg.remoteAddress()));
39+
}
40+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.gportal.source.query;
2+
3+
import static com.gportal.source.query.Message.readString;
4+
import static com.gportal.source.query.Message.writeString;
5+
6+
import io.netty.buffer.ByteBuf;
7+
8+
public record PlayerInfo(byte index, String name, int score, float duration) {
9+
public static PlayerInfo read(ByteBuf buffer) {
10+
byte index = buffer.readByte();
11+
String name = readString(buffer);
12+
int score = buffer.readIntLE();
13+
float duration = buffer.readFloatLE();
14+
15+
return new PlayerInfo(index, name, score, duration);
16+
}
17+
public PlayerInfo write(ByteBuf buffer) {
18+
buffer.writeByte(index());
19+
writeString(buffer, name());
20+
buffer.writeIntLE(score());
21+
buffer.writeFloatLE(duration());
22+
23+
return this;
24+
}
25+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.gportal.source.query;
2+
3+
public interface Query extends Message {
4+
public Integer challenge();
5+
public Query withChallenge(int challenge);
6+
}

0 commit comments

Comments
 (0)