Skip to content

Commit 32db7b8

Browse files
authored
Merge pull request #336 from LossyDragon/smm
Implement SteamMatchMaking
2 parents dc35263 + 9b6d5a9 commit 32db7b8

19 files changed

Lines changed: 1718 additions & 8 deletions

File tree

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package in.dragonbra.javasteamsamples._014_steammatchmaking;
2+
3+
import in.dragonbra.javasteam.enums.ELobbyComparison;
4+
import in.dragonbra.javasteam.enums.ELobbyDistanceFilter;
5+
import in.dragonbra.javasteam.enums.EResult;
6+
import in.dragonbra.javasteam.steam.authentication.*;
7+
import in.dragonbra.javasteam.steam.handlers.steammatchmaking.*;
8+
import in.dragonbra.javasteam.steam.handlers.steamuser.LogOnDetails;
9+
import in.dragonbra.javasteam.steam.handlers.steamuser.SteamUser;
10+
import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOffCallback;
11+
import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOnCallback;
12+
import in.dragonbra.javasteam.steam.steamclient.SteamClient;
13+
import in.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackManager;
14+
import in.dragonbra.javasteam.steam.steamclient.callbacks.ConnectedCallback;
15+
import in.dragonbra.javasteam.steam.steamclient.callbacks.DisconnectedCallback;
16+
import in.dragonbra.javasteam.util.log.DefaultLogListener;
17+
import in.dragonbra.javasteam.util.log.LogManager;
18+
19+
import java.io.Closeable;
20+
import java.io.IOException;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.concurrent.CancellationException;
24+
25+
/**
26+
* @author lossy
27+
* @since 2025-05-21
28+
*/
29+
@SuppressWarnings("FieldCanBeLocal")
30+
public class SampleSteamMatchmaking implements Runnable {
31+
32+
private final Integer appID = 480; // Team Fortress 2
33+
34+
private SteamClient steamClient;
35+
36+
private CallbackManager manager;
37+
38+
private SteamUser steamUser;
39+
40+
private SteamMatchmaking steamMatchmaking;
41+
42+
private boolean isRunning;
43+
44+
private final String user;
45+
46+
private final String pass;
47+
48+
private List<Closeable> subscriptions;
49+
50+
public SampleSteamMatchmaking(String user, String pass) {
51+
this.user = user;
52+
this.pass = pass;
53+
}
54+
55+
public static void main(String[] args) {
56+
if (args.length < 2) {
57+
System.out.println("Sample1: No username and password specified!");
58+
return;
59+
}
60+
61+
LogManager.addListener(new DefaultLogListener());
62+
63+
new SampleSteamMatchmaking(args[0], args[1]).run();
64+
}
65+
66+
@Override
67+
public void run() {
68+
// create our steamclient instance using default configuration
69+
steamClient = new SteamClient();
70+
71+
// create the callback manager which will route callbacks to function calls
72+
manager = new CallbackManager(steamClient);
73+
74+
// get the steamuser handler, which is used for logging on after successfully connecting
75+
steamUser = steamClient.getHandler(SteamUser.class);
76+
77+
// get the steammatchmaking handler.
78+
steamMatchmaking = steamClient.getHandler(SteamMatchmaking.class);
79+
80+
// The callbacks are a closeable, and to properly fix
81+
// "'Closeable' used without 'try'-with-resources statement", they should be closed once done.
82+
// Usually putting them in a list and close each of them once the client is finished is recommended.
83+
subscriptions = new ArrayList<>();
84+
85+
// register a few callbacks we're interested in
86+
// these are registered upon creation to a callback manager, which will then route the callbacks
87+
// to the functions specified
88+
subscriptions.add(manager.subscribe(ConnectedCallback.class, this::onConnected));
89+
subscriptions.add(manager.subscribe(DisconnectedCallback.class, this::onDisconnected));
90+
subscriptions.add(manager.subscribe(LoggedOnCallback.class, this::onLoggedOn));
91+
subscriptions.add(manager.subscribe(LoggedOffCallback.class, this::onLoggedOff));
92+
93+
isRunning = true;
94+
95+
System.out.println("Connecting to steam...");
96+
97+
// initiate the connection
98+
steamClient.connect();
99+
100+
// create our callback handling loop
101+
while (isRunning) {
102+
// in order for the callbacks to get routed, they need to be handled by the manager
103+
manager.runWaitCallbacks(1000L);
104+
}
105+
106+
// Close the subscriptions when done.
107+
System.out.println("Closing " + subscriptions.size() + " callbacks");
108+
for (var subscription : subscriptions) {
109+
try {
110+
subscription.close();
111+
} catch (IOException e) {
112+
System.out.println("Couldn't close a callback.");
113+
}
114+
}
115+
}
116+
117+
@SuppressWarnings("DanglingJavadoc")
118+
private void onConnected(ConnectedCallback callback) {
119+
System.out.println("Connected to Steam! Logging in " + user + "...");
120+
121+
var shouldRememberPassword = false;
122+
123+
AuthSessionDetails authDetails = new AuthSessionDetails();
124+
authDetails.username = user;
125+
authDetails.password = pass;
126+
authDetails.persistentSession = shouldRememberPassword;
127+
128+
/**
129+
* {@link UserConsoleAuthenticator} is the default authenticator implementation provided by JavaSteam
130+
* for ease of use which blocks the thread and asks for user input to enter the code.
131+
* However, if you require special handling (e.g. you have the TOTP secret and can generate codes on the fly),
132+
* you can implement your own {@link IAuthenticator}.
133+
*/
134+
authDetails.authenticator = new UserConsoleAuthenticator();
135+
136+
try {
137+
// Begin authenticating via credentials.
138+
var authSession = steamClient.getAuthentication().beginAuthSessionViaCredentials(authDetails).get();
139+
140+
// Note: This is blocking, it would be up to you to make it non-blocking for Java.
141+
// Note: Kotlin uses should use ".pollingWaitForResult()" as its a suspending function.
142+
AuthPollResult pollResponse = authSession.pollingWaitForResult().get();
143+
144+
// Logon to Steam with the access token we have received
145+
// Note that we are using RefreshToken for logging on here
146+
LogOnDetails details = new LogOnDetails();
147+
details.setUsername(pollResponse.getAccountName());
148+
details.setAccessToken(pollResponse.getRefreshToken());
149+
150+
// Set LoginID to a non-zero value if you have another client connected using the same account,
151+
// the same private ip, and same public ip.
152+
details.setLoginID(149);
153+
154+
steamUser.logOn(details);
155+
156+
} catch (Exception e) {
157+
// List a couple of exceptions that could be important to handle.
158+
if (e instanceof AuthenticationException) {
159+
System.err.println("An Authentication error has occurred. " + e.getMessage());
160+
} else if (e instanceof CancellationException) {
161+
System.err.println("An Cancellation exception was raised. Usually means a timeout occurred. " + e.getMessage());
162+
} else {
163+
System.err.println("An error occurred:" + e.getMessage());
164+
}
165+
166+
steamUser.logOff();
167+
}
168+
}
169+
170+
private void onDisconnected(DisconnectedCallback callback) {
171+
System.out.println("Disconnected from Steam. User initialized: " + callback.isUserInitiated());
172+
173+
// If the disconnection was not user initiated, we will retry connecting to steam again after a short delay.
174+
if (callback.isUserInitiated()) {
175+
isRunning = false;
176+
} else {
177+
try {
178+
Thread.sleep(2000L);
179+
steamClient.connect();
180+
} catch (InterruptedException e) {
181+
System.err.println("An Interrupted exception occurred. " + e.getMessage());
182+
}
183+
}
184+
}
185+
186+
private void onLoggedOn(LoggedOnCallback callback) {
187+
if (callback.getResult() != EResult.OK) {
188+
System.out.println("Unable to logon to Steam: " + callback.getResult() + " / " + callback.getExtendedResult());
189+
190+
isRunning = false;
191+
return;
192+
}
193+
194+
System.out.println("Successfully logged on!");
195+
196+
// at this point, we'd be able to perform actions on Steam
197+
198+
try {
199+
var filters = List.of(
200+
new DistanceFilter(ELobbyDistanceFilter.Worldwide),
201+
new StringFilter("CONMETHOD", "P2P", ELobbyComparison.Equal)
202+
);
203+
var lobbyListCallback = steamMatchmaking.getLobbyList(appID, filters, 20).toFuture().get();
204+
205+
System.out.println("App ID: " + lobbyListCallback.getAppID());
206+
System.out.println("Result: " + lobbyListCallback.getResult());
207+
System.out.println("Lobby Size: " + lobbyListCallback.getLobbies().size());
208+
lobbyListCallback.getLobbies().forEach(lobby -> {
209+
System.out.println("\tsteamID: " + lobby.getSteamID().convertToUInt64());
210+
System.out.println("\tlobbyType: " + lobby.getLobbyType());
211+
System.out.println("\tlobbyFlags: " + lobby.getLobbyFlags());
212+
System.out.println("\townerSteamID: " + lobby.getOwnerSteamID());
213+
214+
System.out.println("\tMetadata:");
215+
lobby.getMetadata().forEach((k, v) -> System.out.println("\t\tkey: " + k + " value: " + v));
216+
217+
System.out.println("\tmaxMembers: " + lobby.getMaxMembers());
218+
System.out.println("\tnumMembers: " + lobby.getNumMembers());
219+
220+
System.out.println("\tMembers:");
221+
lobby.getMembers().forEach(member -> {
222+
System.out.println("\t\tsteamID: " + member.getSteamID().convertToUInt64());
223+
System.out.println("\t\tpersonaName: " + member.getPersonaName());
224+
System.out.println("\t\tMember Metadata:");
225+
member.getMetadata().forEach((k, v) ->
226+
System.out.println("\t\t\tkey: " + k + " value: " + v));
227+
});
228+
229+
System.out.println("\tdistance: " + lobby.getDistance());
230+
System.out.println("\tweight: " + lobby.getWeight());
231+
System.out.println("\n");
232+
});
233+
234+
235+
} catch (Exception e) {
236+
System.err.println(e.getMessage());
237+
} finally {
238+
steamUser.logOff();
239+
}
240+
}
241+
242+
private void onLoggedOff(LoggedOffCallback callback) {
243+
System.out.println("Logged off of Steam: " + callback.getResult());
244+
245+
isRunning = false;
246+
}
247+
}

src/main/java/in/dragonbra/javasteam/steam/CMClient.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@
1818
import in.dragonbra.javasteam.steam.discovery.SmartCMServerList;
1919
import in.dragonbra.javasteam.steam.steamclient.configuration.SteamConfiguration;
2020
import in.dragonbra.javasteam.types.SteamID;
21-
import in.dragonbra.javasteam.util.IDebugNetworkListener;
22-
import in.dragonbra.javasteam.util.MsgUtil;
23-
import in.dragonbra.javasteam.util.NetHookNetworkListener;
24-
import in.dragonbra.javasteam.util.Strings;
21+
import in.dragonbra.javasteam.util.*;
2522
import in.dragonbra.javasteam.util.event.EventArgs;
2623
import in.dragonbra.javasteam.util.event.EventHandler;
2724
import in.dragonbra.javasteam.util.event.ScheduledFunction;
@@ -48,6 +45,12 @@ public abstract class CMClient {
4845

4946
private final SteamConfiguration configuration;
5047

48+
@Nullable
49+
private InetAddress publicIP;
50+
51+
@Nullable
52+
private String ipCountryCode;
53+
5154
private boolean isConnected;
5255

5356
private long sessionToken;
@@ -425,6 +428,8 @@ private void handleLogOnResponse(IPacketMsg packetMsg) {
425428
steamID = new SteamID(logonResp.getProtoHeader().getSteamid());
426429

427430
cellID = logonResp.getBody().getCellId();
431+
publicIP = NetHelpers.getIPAddress(logonResp.getBody().getPublicIp());
432+
ipCountryCode = logonResp.getBody().getIpCountryCode();
428433

429434
// restart heartbeat
430435
heartBeatFunc.stop();
@@ -445,6 +450,8 @@ private void handleLoggedOff(IPacketMsg packetMsg) {
445450
steamID = null;
446451

447452
cellID = null;
453+
publicIP = null;
454+
ipCountryCode = null;
448455

449456
heartBeatFunc.stop();
450457

@@ -519,6 +526,26 @@ public SmartCMServerList getServers() {
519526
return connection.getCurrentEndPoint();
520527
}
521528

529+
/**
530+
* Gets the public IP address of this client. This value is assigned after a logon attempt has succeeded.
531+
* This value will be <c>null</c> if the client is logged off of Steam.
532+
*
533+
* @return The {@link InetSocketAddress} public ip
534+
*/
535+
public @Nullable InetAddress getPublicIP() {
536+
return publicIP;
537+
}
538+
539+
/**
540+
* Gets the country code of our public IP address according to Steam. This value is assigned after a logon attempt has succeeded.
541+
* This value will be <c>null</c> if the client is logged off of Steam.
542+
*
543+
* @return the ip country code.
544+
*/
545+
public @Nullable String getIpCountryCode() {
546+
return ipCountryCode;
547+
}
548+
522549
/**
523550
* Gets the universe of this client.
524551
*

src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ChatMemberInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() {
3535
val type: EChatInfoType
3636

3737
/**
38-
* Gets the state change info for <see cref="EChatInfoType.StateChange"/> member info updates.
38+
* Gets the state change info for [EChatInfoType.StateChange] member info updates.
3939
*/
4040
var stateChangeInfo: StateChangeDetails? = null
4141

0 commit comments

Comments
 (0)