Skip to content

Commit

Permalink
Add Spectator Client+Server
Browse files Browse the repository at this point in the history
  • Loading branch information
ankbhatia19 authored and raimannma committed Oct 7, 2024
1 parent 93639a2 commit 2237250
Show file tree
Hide file tree
Showing 50 changed files with 49,698 additions and 0 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/docker-image-spectator.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: docker-image

on:
workflow_dispatch:
push:
branches:
- 'master'
paths:
- 'spectator/**'

env:
IMAGE_TAG_OWNER: opensource-deadlock-tools
IMAGE_TAG_NAME: devlock

permissions:
contents: read
packages: write

concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}

jobs:
docker:
strategy:
matrix:
PATH: [spectator/server, spectator/client]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Compose
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ env.IMAGE_TAG_OWNER }}/${{ env.IMAGE_TAG_NAME }}/${{ matrix.PATH }}:latest
context: ${{ matrix.PATH }}
4 changes: 4 additions & 0 deletions spectator/client/.env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SPECTATOR_STEAM_ACCOUNTS='[
{"username": "user1", "password": "pass1"},
{"username": "user2", "password": "pass2"}
]'
1 change: 1 addition & 0 deletions spectator/client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker-compose.yaml
15 changes: 15 additions & 0 deletions spectator/client/DeadlockAPI/DeadlockAPI.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="protobuf-net" Version="3.2.30" />
<PackageReference Include="Snappier" Version="1.1.6" />
<PackageReference Include="SteamKit2" Version="2.5.0" />
</ItemGroup>

</Project>
222 changes: 222 additions & 0 deletions spectator/client/DeadlockAPI/DeadlockClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
using DeadlockAPI.Enums;
using ouwou.GC.Deadlock.Internal;
using SteamKit2;
using SteamKit2.Authentication;
using SteamKit2.GC;
using SteamKit2.Internal;

namespace DeadlockAPI {
public class DeadlockClient {
SteamClient client;

SteamUser user;
SteamGameCoordinator gameCoordinator;

CallbackManager callbackMgr;

string userName;
string password;
string previouslyStoredGuardData;

bool disconnecting = false;

const int APPID = 1422450;

uint clientVersion = 0;

public DeadlockClient(string userName, string password) {
this.userName = userName;
this.password = password;

client = new SteamClient();

user = client.GetHandler<SteamUser>();
gameCoordinator = client.GetHandler<SteamGameCoordinator>();

callbackMgr = new CallbackManager(client);
callbackMgr.Subscribe<SteamClient.ConnectedCallback>(OnConnected);
callbackMgr.Subscribe<SteamClient.DisconnectedCallback>(OnDisconnected);
callbackMgr.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
callbackMgr.Subscribe<SteamGameCoordinator.MessageCallback>(OnGCMessage);

if (File.Exists("guard.txt")) {
previouslyStoredGuardData = File.ReadAllText("guard.txt");
}
}

public bool IsConnected { get => client.IsConnected; }

public void Connect() {
Console.WriteLine("[*] Connecting to Steam");
disconnecting = false;
client.Connect();
}

public void Disconnect() {
disconnecting = true;
client.Disconnect();
}

public void Wait() {
while (true) {
callbackMgr.RunWaitCallbacks(TimeSpan.FromSeconds(1));
}
}

public void RunCallbacks(TimeSpan t) {
callbackMgr.RunWaitCallbacks(t);
}

public void Start()
{
Connect();

// Run Wait() on a separate thread
_ = Task.Run(() => Wait());
}

async void OnConnected(SteamClient.ConnectedCallback callback) {
Console.WriteLine("[*] Connected. Logging into Steam as {0}", userName);

var authSession = await client.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails {
Username = userName,
Password = password,
IsPersistentSession = true,
GuardData = previouslyStoredGuardData,
Authenticator = new UserConsoleAuthenticator(),
});
var pollResponse = await authSession.PollingWaitForResultAsync();
if (pollResponse.NewGuardData != null) {
previouslyStoredGuardData = pollResponse.NewGuardData;
File.WriteAllText("guard.txt", previouslyStoredGuardData);
}
user.LogOn(new SteamUser.LogOnDetails {
Username = pollResponse.AccountName,
AccessToken = pollResponse.RefreshToken,
ShouldRememberPassword = true,
});
}

void OnDisconnected(SteamClient.DisconnectedCallback callback) {
if (!disconnecting) {
Console.WriteLine("[*] Could not connect.. Retrying..");
Thread.Sleep(5000);
Connect();
}
}

void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
if (callback.Result != EResult.OK) {
Console.Error.WriteLine("[*] Unable to log on to Steam: {0}", callback.Result);
return;
}

Console.WriteLine("[*] Logged in. Launching Deadlock");

var playGame = new ClientMsgProtobuf<CMsgClientGamesPlayed>(EMsg.ClientGamesPlayed);
playGame.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed {
game_id = new GameID(APPID)
});

client.Send(playGame);

Thread.Sleep(5000);

var clientHello = new ClientGCMsgProtobuf<CMsgCitadelClientHello>((uint)EGCBaseClientMsg.k_EMsgGCClientHello);
clientHello.Body.region_mode = ECitadelRegionMode.k_ECitadelRegionMode_ROW;
gameCoordinator.Send(clientHello, APPID);
}

void OnGCMessage(SteamGameCoordinator.MessageCallback callback) {
var messageMap = new Dictionary<uint, Action<IPacketGCMsg>>
{
{ (uint) EGCBaseClientMsg.k_EMsgGCClientWelcome, OnClientWelcome },
{ (uint) EGCCitadelClientMessages.k_EMsgGCToClientDevPlaytestStatus, OnDevPlaytestStatus },
};

Action<IPacketGCMsg> func;
if (!messageMap.TryGetValue(callback.EMsg, out func)) {
return;
}

func(callback.Message);
}

public async Task<U?> SendAndReceiveWithJob<T, U>(ClientGCMsgProtobuf<T> msg)
where T : ProtoBuf.IExtensible, new()
where U : ProtoBuf.IExtensible, new() {
msg.SourceJobID = client.GetNextJobID();
gameCoordinator.Send(msg, APPID);
try {
var cb = await new AsyncJob<SteamGameCoordinator.MessageCallback>(client, msg.SourceJobID);
var response = new ClientGCMsgProtobuf<U>(cb.Message);

return response.Body;
} catch (Exception e) {
Console.Write(e.ToString());
return default;
}
}

public class MatchMetaData {
public required CMsgClientToGCGetMatchMetaDataResponse Data;
public required string ReplayURL;
public required string MetadataURL;
}

public async Task<MatchMetaData?> GetMatchMetaData(uint matchId) {
var msg = new ClientGCMsgProtobuf<CMsgClientToGCGetMatchMetaData>((uint)EGCCitadelClientMessages.k_EMsgClientToGCGetMatchMetaData);
msg.Body.match_id = matchId;
var r = await SendAndReceiveWithJob<CMsgClientToGCGetMatchMetaData, CMsgClientToGCGetMatchMetaDataResponse>(msg);
if (r == null) return null;
return new MatchMetaData() {
Data = r,
ReplayURL = $"http://replay{r.cluster_id}.valve.net/{APPID}/{matchId}_{r.replay_salt}.dem.bz2",
MetadataURL = $"http://replay{r.cluster_id}.valve.net/{APPID}/{matchId}_{r.metadata_salt}.meta.bz2"
};
}

public async Task<CMsgClientToGCSpectateLobbyResponse?> SpectateLobby(uint matchId) {
var msg = new ClientGCMsgProtobuf<CMsgClientToGCSpectateLobby>((uint)EGCCitadelClientMessages.k_EMsgClientToGCSpectateLobby);
msg.Body.match_id = matchId;
msg.Body.client_version = clientVersion;
msg.Body.client_platform = EGCPlatform.k_eGCPlatform_PC;
return await SendAndReceiveWithJob<CMsgClientToGCSpectateLobby, CMsgClientToGCSpectateLobbyResponse>(msg);
}

public async Task<CMsgClientToGCFindHeroBuildsResponse?> FindHeroBuilds(Heroes hero) {
var msg = new ClientGCMsgProtobuf<CMsgClientToGCFindHeroBuilds>((uint)EGCCitadelClientMessages.k_EMsgClientToGCFindHeroBuilds);
msg.Body.hero_id = (uint)hero;
return await SendAndReceiveWithJob<CMsgClientToGCFindHeroBuilds, CMsgClientToGCFindHeroBuildsResponse>(msg);
}

public async Task<CMsgClientToGCGetActiveMatchesResponse?> GetActiveMatches() {
var msg = new ClientGCMsgProtobuf<CMsgClientToGCGetActiveMatches>((uint)EGCCitadelClientMessages.k_EMsgClientToGCGetActiveMatches);
msg.SourceJobID = client.GetNextJobID();
gameCoordinator.Send(msg, APPID);
var cb = await new AsyncJob<SteamGameCoordinator.MessageCallback>(client, msg.SourceJobID);
var decompressed = Snappier.Snappy.DecompressToArray(new ReadOnlySpan<byte>(cb.Message.GetData(), 24, cb.Message.GetData().Length - 24));
return ProtoBuf.Serializer.Deserialize<CMsgClientToGCGetActiveMatchesResponse>(new ReadOnlySpan<byte>(decompressed));
}

public class ClientWelcomeEventArgs : EventArgs {
public required CMsgClientWelcome Data;
}
public event EventHandler<ClientWelcomeEventArgs> ClientWelcomeEvent;
void OnClientWelcome(IPacketGCMsg packetMsg) {
var msg = new ClientGCMsgProtobuf<CMsgClientWelcome>(packetMsg);
clientVersion = msg.Body.version;
Console.WriteLine($"[*] Deadlock Client v{clientVersion}");
ClientWelcomeEvent?.Invoke(this, new ClientWelcomeEventArgs() { Data = msg.Body });
}

public class DevPlaytestStatusEventArgs : EventArgs {
public required CMsgGCToClientDevPlaytestStatus Data;
}
public event EventHandler<DevPlaytestStatusEventArgs> DevPlaytestStatusEvent;
void OnDevPlaytestStatus(IPacketGCMsg packetMsg) {
var msg = new ClientGCMsgProtobuf<CMsgGCToClientDevPlaytestStatus>(packetMsg);
DevPlaytestStatusEvent?.Invoke(this, new DevPlaytestStatusEventArgs() { Data = msg.Body });
}
}
}
33 changes: 33 additions & 0 deletions spectator/client/DeadlockAPI/Enums/Heroes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DeadlockAPI.Enums
{
public enum Heroes
{
Abrams = 6,
Bebop = 15,
Dynamo = 11,
GreyTalon = 17,
Haze = 13,
Infernus = 1,
Ivy = 20,
Kelvin = 12,
LadyGeist = 4,
Lash = 31,
McGinnis = 8,
MoAndKrill = 18,
Paradox = 10,
Pocket = 50,
Seven = 2,
Shiv = 19,
Vindicta = 3,
Viscous = 35,
Warden = 25,
Wraith = 7,
Yamato = 27,
}
}
14 changes: 14 additions & 0 deletions spectator/client/DeadlockAPI/Enums/MatchResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DeadlockAPI.Enums
{
public enum MatchResult
{
Win = 1,
Loss = 0,
}
}
8 changes: 8 additions & 0 deletions spectator/client/DeadlockAPI/Enums/Teams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DeadlockAPI.Enums
{
public enum Teams
{
Team0 = 0,
Team1 = 1
}
}
Loading

0 comments on commit 2237250

Please sign in to comment.