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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/Progressive-Java-Client.jar
64 changes: 31 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,41 @@
# Java 254 Client
# Progressive Java Client

Full RS2 build-254 client With Native OpenGL-lwjgl
Early Java desktop client for a LostCityRS / 2004SP revision 254+ servers.

## Build

```bat
build.bat
```

Or with PowerShell:

```powershell
.\build.ps1
```
## Current status

Requires JDK 17+.
Implemented so far:

The generated `target/Progressive-Java-Client.jar` is standalone: it contains the
required libraries and native binaries, and can be copied elsewhere by itself.
- Revision `254` configuration
- Swing desktop window
- 765x503 game canvas
- 50 TPS game loop
- WebSocket connection to the game server
- `/crc` cache checksum loading
- 254 login handshake using opcodes `14`, `16`, and `18`
- ISAAC cipher setup
- Client/server protocol constants
- Test login UI

You can also build with Maven:
Not implemented yet:

```powershell
mvn clean package
```
- 117HD-style rendering port
- HD terrain lighting and shading
- HD textures and normal maps
- Water, lava, and animated surface effects
- Improved skybox/fog/atmosphere rendering
- Modern GPU-based scene rendering
- 60 FPS animation/camera support while keeping server TPS compatible
- True resizable and fullscreen client modes
- Original 765x503 fixed-mode compatibility

The standalone JAR is written to `target/`.
## Requirements

## Run
- Java 17+
- Maven
- A compatible LostCity/2004Scape revision 254 server running locally or remotely (Progressive strongly advised)

```bat
run.bat
```

Using `run.bat` is recommended because it supplies the Java options used by
LWJGL and starts the client with the default server settings.

You can also launch the generated JAR or a GitHub Release JAR directly:
## Build

```powershell
java -jar Progressive-Java-Client.jar
```
```bash
mvn package
Binary file added cache/jingle1.mid
Binary file not shown.
Binary file added cache/jingle2.mid
Binary file not shown.
Binary file added cache/jingle3.mid
Binary file not shown.
Binary file added cache/jingle4.mid
Binary file not shown.
Binary file added cache/sound0.wav
Binary file not shown.
Binary file added cache/sound1.wav
Binary file not shown.
Binary file added cache/sound2.wav
Binary file not shown.
Binary file added cache/sound3.wav
Binary file not shown.
Binary file added cache/sound4.wav
Binary file not shown.
127 changes: 127 additions & 0 deletions src/main/java/com/gradwahl/rs254/discord/DiscordRichPresence.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.gradwahl.rs254.discord;

import java.io.Closeable;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public final class DiscordRichPresence implements Closeable {
private static final int OP_HANDSHAKE = 0;
private static final int OP_FRAME = 1;
private static final int OP_CLOSE = 2;

private final String appId;
private final ExecutorService worker = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "discord-rpc");
t.setDaemon(true);
return t;
});
private final AtomicInteger nonce = new AtomicInteger();

private RandomAccessFile pipe;
private long startTime;
private boolean connected;

public DiscordRichPresence(String appId) {
this.appId = appId;
}

public void connect() {
worker.execute(() -> {
disconnectNow();
startTime = System.currentTimeMillis() / 1000L;
for (int i = 0; i < 10; i++) {
RandomAccessFile candidate = null;
try {
candidate = new RandomAccessFile("\\\\.\\pipe\\discord-ipc-" + i, "rw");
writePacket(candidate, OP_HANDSHAKE, "{\"v\":1,\"client_id\":\"" + escape(appId) + "\"}");
readPacket(candidate);
pipe = candidate;
connected = true;
return;
} catch (IOException ignored) {
if (candidate != null) {
try {
candidate.close();
} catch (IOException ignoredClose) {
}
}
}
}
System.err.println("[discord] could not connect to Discord IPC (is Discord running?)");
});
}

public void updateActivity(String details, String state) {
worker.execute(() -> {
if (!connected || pipe == null) return;
String activity = "{\"details\":\"" + escape(details) + "\""
+ ",\"state\":\"" + escape(state) + "\""
+ ",\"timestamps\":{\"start\":" + startTime + "}"
+ ",\"assets\":{\"large_image\":\"logo\",\"large_text\":\"LostCity RSPS\"}}";
String payload = "{\"cmd\":\"SET_ACTIVITY\""
+ ",\"args\":{\"pid\":" + ProcessHandle.current().pid() + ",\"activity\":" + activity + "}"
+ ",\"nonce\":\"" + nonce.incrementAndGet() + "\"}";
try {
writePacket(pipe, OP_FRAME, payload);
readPacket(pipe);
} catch (IOException e) {
disconnectNow();
}
});
}

public void disconnect() {
worker.execute(this::disconnectNow);
}

private void disconnectNow() {
RandomAccessFile f = pipe;
pipe = null;
connected = false;
if (f == null) return;
try {
writePacket(f, OP_CLOSE, "{}");
} catch (IOException ignored) {
}
try {
f.close();
} catch (IOException ignored) {
}
}

private static void writePacket(RandomAccessFile f, int opcode, String payload) throws IOException {
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
ByteBuffer header = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
header.putInt(opcode);
header.putInt(data.length);
f.write(header.array());
f.write(data);
}

private static void readPacket(RandomAccessFile f) throws IOException {
byte[] header = new byte[8];
f.readFully(header);
int length = ByteBuffer.wrap(header, 4, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
if (length > 0 && length <= 65_535) {
byte[] body = new byte[length];
f.readFully(body);
}
}

private static String escape(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}

@Override
public void close() {
disconnect();
worker.shutdownNow();
}
}
Loading
Loading