From 35892b489878527758939a1424b39b951f67a8ef Mon Sep 17 00:00:00 2001 From: extio-de Date: Sun, 5 Jul 2026 16:32:26 +0200 Subject: [PATCH] feat(Steamworks): add auto-configuration for Steamworks integration and update README --- README.md | 18 +++- pom.xml | 2 +- .../SteamworksAutoConfiguration.java | 28 ++++++ .../steamworks/SteamworksConnectorImpl.java | 19 ---- .../steamworks/SteamworksRunner.java | 93 +++++++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + 6 files changed, 138 insertions(+), 23 deletions(-) create mode 100644 src/main/java/de/extio/game_engine/steamworks/SteamworksAutoConfiguration.java create mode 100644 src/main/java/de/extio/game_engine/steamworks/SteamworksRunner.java diff --git a/README.md b/README.md index 2b80fc6..178f7c9 100644 --- a/README.md +++ b/README.md @@ -1107,13 +1107,25 @@ No additional configuration required. #### Purpose and Overview Provides integration with the Steam platform API through a connector abstraction. Supports Steam features including user identity, Steam overlay, lobbies, achievements, and rich presence. The connector is designed as a singleton and can be optionally initialized when running on Steam. -**Note**: This is an optional component that requires the Steamworks4J library and proper Steam initialization. +**Note**: This is an optional component that requires the Steamworks4J library and proper Steam initialization. It is **disabled by default** and must be explicitly enabled via configuration property. #### Setup / Autoconfiguration -No auto-configuration provided. Must be manually initialized by setting `SteamworksConnector.setInstance()` with a concrete implementation (e.g., `SteamworksConnectorImpl`). +**Configuration Class**: `SteamworksAutoConfiguration` + +**Property**: `game-engine.steamworks.enabled` (default: `false`) + +The auto-configuration is guarded by two conditions: +- `game-engine.steamworks.enabled=true` must be set +- The `com.codedisaster.steamworks.SteamAPI` class must be present on the classpath (`@ConditionalOnClass`) + +When enabled, `SteamworksConnectorImpl` is instantiated, registered as the singleton via `SteamworksConnector.setInstance()`, and a `SteamworksRunner` thread (implementing `SmartLifecycle`) is started to drive the Steam callback loop. The runner has a phase of `Integer.MAX_VALUE` (starts last, stops first) and will gracefully shut down the Steam API on context close. #### Exposed Spring Beans -None (managed as a singleton via static accessor) +- **`SteamworksConnector`** (via `SteamworksConnectorImpl`): The Steam API connector singleton +- **`SteamworksRunner`**: Lifecycle-managed callback runner thread + +#### Integration Notes +If a custom `SteamworksConnector` bean is provided, the auto-configuration will not create `SteamworksConnectorImpl` but will still create the `SteamworksRunner` to drive callbacks. Both beans use `@ConditionalOnMissingBean`. --- diff --git a/pom.xml b/pom.xml index 5ed8fe2..7e5ac0a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 de.extio game-engine - 1.0.24 + 1.0.25 25 diff --git a/src/main/java/de/extio/game_engine/steamworks/SteamworksAutoConfiguration.java b/src/main/java/de/extio/game_engine/steamworks/SteamworksAutoConfiguration.java new file mode 100644 index 0000000..d09f861 --- /dev/null +++ b/src/main/java/de/extio/game_engine/steamworks/SteamworksAutoConfiguration.java @@ -0,0 +1,28 @@ +package de.extio.game_engine.steamworks; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +@ConditionalOnProperty(name = "game-engine.steamworks.enabled", havingValue = "true") +@ConditionalOnClass(name = "com.codedisaster.steamworks.SteamAPI") +public class SteamworksAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(SteamworksConnector.class) + SteamworksConnectorImpl steamworksConnector() { + final var connector = new SteamworksConnectorImpl(); + SteamworksConnector.setInstance(connector); + return connector; + } + + @Bean + @ConditionalOnMissingBean + SteamworksRunner steamworksRunner(final SteamworksConnector connector) { + return new SteamworksRunner(connector); + } + +} diff --git a/src/main/java/de/extio/game_engine/steamworks/SteamworksConnectorImpl.java b/src/main/java/de/extio/game_engine/steamworks/SteamworksConnectorImpl.java index fcb0e37..e7567c4 100644 --- a/src/main/java/de/extio/game_engine/steamworks/SteamworksConnectorImpl.java +++ b/src/main/java/de/extio/game_engine/steamworks/SteamworksConnectorImpl.java @@ -1,12 +1,8 @@ package de.extio.game_engine.steamworks; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -17,7 +13,6 @@ import com.codedisaster.steamworks.SteamAuth.AuthSessionResponse; import com.codedisaster.steamworks.SteamAuthTicket; import com.codedisaster.steamworks.SteamFriends; import com.codedisaster.steamworks.SteamFriends.OverlayDialog; -import com.codedisaster.steamworks.SteamFriends.OverlayToWebPageMode; import com.codedisaster.steamworks.SteamFriends.PersonaChange; import com.codedisaster.steamworks.SteamFriendsCallback; import com.codedisaster.steamworks.SteamID; @@ -29,18 +24,9 @@ import com.codedisaster.steamworks.SteamMatchmaking.ChatMemberStateChange; import com.codedisaster.steamworks.SteamMatchmaking.ChatRoomEnterResponse; import com.codedisaster.steamworks.SteamMatchmaking.LobbyType; import com.codedisaster.steamworks.SteamMatchmakingCallback; -import com.codedisaster.steamworks.SteamNativeHandle; import com.codedisaster.steamworks.SteamPublishedFileID; -import com.codedisaster.steamworks.SteamRemoteStorage; -import com.codedisaster.steamworks.SteamRemoteStorage.PublishedFileVisibility; import com.codedisaster.steamworks.SteamResult; import com.codedisaster.steamworks.SteamUGC; -import com.codedisaster.steamworks.SteamUGC.ItemInstallInfo; -import com.codedisaster.steamworks.SteamUGC.ItemUpdateInfo; -import com.codedisaster.steamworks.SteamUGC.ItemUpdateStatus; -import com.codedisaster.steamworks.SteamUGCCallback; -import com.codedisaster.steamworks.SteamUGCDetails; -import com.codedisaster.steamworks.SteamUGCQuery; import com.codedisaster.steamworks.SteamUGCUpdateHandle; import com.codedisaster.steamworks.SteamUser; import com.codedisaster.steamworks.SteamUserCallback; @@ -115,11 +101,6 @@ public final class SteamworksConnectorImpl extends SteamworksConnector { @Override public void runCallbacks() { - - - // TODO: Executor thread - - if (!this.active) { return; } diff --git a/src/main/java/de/extio/game_engine/steamworks/SteamworksRunner.java b/src/main/java/de/extio/game_engine/steamworks/SteamworksRunner.java new file mode 100644 index 0000000..1fc745a --- /dev/null +++ b/src/main/java/de/extio/game_engine/steamworks/SteamworksRunner.java @@ -0,0 +1,93 @@ +package de.extio.game_engine.steamworks; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.context.SmartLifecycle; + +public final class SteamworksRunner implements SmartLifecycle { + + private static final Logger LOGGER = LogManager.getLogger(); + + private static final int SLEEP_MS = 33; + + private final SteamworksConnector connector; + + private final AtomicBoolean running = new AtomicBoolean(); + + private Thread thread; + + public SteamworksRunner(final SteamworksConnector connector) { + this.connector = connector; + } + + @Override + public void start() { + if (this.running.compareAndSet(false, true)) { + LOGGER.debug(() -> "SteamworksRunner starting"); + + this.thread = new Thread(this::run, "SteamworksRunner"); + this.thread.setDaemon(true); + this.thread.start(); + } + } + + @Override + public void stop() { + if (this.running.compareAndSet(true, false)) { + LOGGER.debug(() -> "SteamworksRunner stopping"); + final Thread t = this.thread; + if (t != null) { + t.interrupt(); + try { + t.join(3000); + } + catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(final Runnable callback) { + this.stop(); + callback.run(); + } + + @Override + public int getPhase() { + return Integer.MAX_VALUE; + } + + private void run() { + try { + while (this.running.get() && this.connector.isActive()) { + this.connector.runCallbacks(); + Thread.sleep(SLEEP_MS); + } + } + catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + catch (final Throwable t) { + LOGGER.warn("Error in SteamworksRunner", t); + } + finally { + this.connector.shutdown(); + LOGGER.debug(() -> "SteamworksRunner stopped"); + } + } + +} diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 1041b78..6c576bf 100644 --- a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -8,3 +8,4 @@ de.extio.game_engine.i18n.LocalizationAutoConfiguration de.extio.game_engine.resource.StaticResourceAutoConfiguration de.extio.game_engine.audio.AudioAutoConfiguration de.extio.game_engine.menu.MenuAutoConfiguration +de.extio.game_engine.steamworks.SteamworksAutoConfiguration