workflowSteps) {
+ this.workflowSteps = workflowSteps;
+ return this;
+ }
+
+ public UpdateBlueprintRequest build() {
+ return new UpdateBlueprintRequest(this);
+ }
+ }
+}
diff --git a/api/src/main/java/app/simplecloud/api/cache/EntityType.java b/api/src/main/java/app/simplecloud/api/cache/EntityType.java
index 8d24484..e4d72d0 100644
--- a/api/src/main/java/app/simplecloud/api/cache/EntityType.java
+++ b/api/src/main/java/app/simplecloud/api/cache/EntityType.java
@@ -7,6 +7,7 @@
public enum EntityType {
SERVER,
GROUP,
+ BLUEPRINT,
PERSISTENT_SERVER,
PLAYER
}
diff --git a/api/src/main/java/app/simplecloud/api/cache/QueryCache.java b/api/src/main/java/app/simplecloud/api/cache/QueryCache.java
index 9f58657..b6c00ea 100644
--- a/api/src/main/java/app/simplecloud/api/cache/QueryCache.java
+++ b/api/src/main/java/app/simplecloud/api/cache/QueryCache.java
@@ -84,7 +84,7 @@ public interface QueryCache {
* For example, {@code invalidateAll(QueryKey.of("servers"))} invalidates:
*
* - {@code QueryKey.of("servers")}
- * - {@code QueryKey.of("servers", "group", "Lobby")}
+ * - {@code QueryKey.of("servers", "serverBaseName", "Lobby")}
* - {@code QueryKey.of("servers", "query", ...)}
*
*
diff --git a/api/src/main/java/app/simplecloud/api/cache/QueryKey.java b/api/src/main/java/app/simplecloud/api/cache/QueryKey.java
index f2c81df..efcec6b 100644
--- a/api/src/main/java/app/simplecloud/api/cache/QueryKey.java
+++ b/api/src/main/java/app/simplecloud/api/cache/QueryKey.java
@@ -12,7 +12,7 @@
* Example keys:
*
{@code
* QueryKey.of("server", serverId) // Single server by ID
- * QueryKey.of("servers", "group", groupName) // Servers filtered by group
+ * QueryKey.of("servers", "serverBaseName", serverBaseName) // Servers filtered by group or persistent server name
* QueryKey.of("servers") // All servers
* QueryKey.of("groups") // All groups
* QueryKey.of("group", groupId) // Single group by ID
@@ -21,7 +21,7 @@
* Hierarchical matching for invalidation:
*
{@code
* // Invalidating QueryKey.of("servers") will also invalidate:
- * // - QueryKey.of("servers", "group", "Lobby")
+ * // - QueryKey.of("servers", "serverBaseName", "Lobby")
* // - QueryKey.of("servers", "query", ...)
* cache.invalidateAll(QueryKey.of("servers"));
* }
@@ -55,6 +55,7 @@ public EntityType getEntityType() {
return switch (first) {
case "server", "servers" -> EntityType.SERVER;
case "group", "groups" -> EntityType.GROUP;
+ case "blueprint", "blueprints" -> EntityType.BLUEPRINT;
case "persistentServer", "persistentServers" -> EntityType.PERSISTENT_SERVER;
case "player", "players" -> EntityType.PLAYER;
default -> null;
@@ -72,7 +73,7 @@ public Object[] getParts() {
* Checks if this key matches or is a parent of the given key.
* Used for invalidation patterns.
*
- * Example: {@code QueryKey.of("servers")} matches {@code QueryKey.of("servers", "group", "Lobby")}
+ *
Example: {@code QueryKey.of("servers")} matches {@code QueryKey.of("servers", "serverBaseName", "Lobby")}
*
* @param other the key to check against
* @return true if this key is a prefix of the other key
diff --git a/api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java b/api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java
index 5e467b8..f358ed7 100644
--- a/api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java
+++ b/api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java
@@ -2,10 +2,12 @@
import app.simplecloud.api.CloudApi;
import app.simplecloud.api.CloudApiOptions;
+import app.simplecloud.api.blueprint.BlueprintApi;
import app.simplecloud.api.cache.CacheConfig;
import app.simplecloud.api.cache.QueryCache;
import app.simplecloud.api.event.EventApi;
import app.simplecloud.api.group.GroupApi;
+import app.simplecloud.api.internal.blueprint.BlueprintApiImpl;
import app.simplecloud.api.internal.cache.CacheEventListener;
import app.simplecloud.api.internal.cache.NoOpQueryCache;
import app.simplecloud.api.internal.cache.QueryCacheImpl;
@@ -33,6 +35,7 @@ public class CloudApiImpl implements CloudApi {
private final CacheEventListener cacheEventListener;
private final ServerApi serverApi;
private final GroupApi groupApi;
+ private final BlueprintApi blueprintApi;
private final PersistentServerApi persistentServerApi;
private final EventApi eventApi;
private final PlayerApi playerApi;
@@ -65,6 +68,7 @@ public CloudApiImpl(CloudApiOptions options) {
this.eventApi = new EventApiImpl(natsClient, options.getNetworkId());
this.serverApi = new ServerApiImpl(options, queryCache);
this.groupApi = new GroupApiImpl(options, queryCache);
+ this.blueprintApi = new BlueprintApiImpl(options, queryCache);
this.persistentServerApi = new PersistentServerApiImpl(options, queryCache);
this.playerApi = new PlayerApiImpl(options, natsClient);
@@ -88,6 +92,11 @@ public ServerApi server() {
return serverApi;
}
+ @Override
+ public BlueprintApi blueprint() {
+ return blueprintApi;
+ }
+
@Override
public PersistentServerApi persistentServer() {
return persistentServerApi;
diff --git a/api/src/main/java/app/simplecloud/api/internal/blueprint/BlueprintApiImpl.java b/api/src/main/java/app/simplecloud/api/internal/blueprint/BlueprintApiImpl.java
new file mode 100644
index 0000000..3fbac65
--- /dev/null
+++ b/api/src/main/java/app/simplecloud/api/internal/blueprint/BlueprintApiImpl.java
@@ -0,0 +1,204 @@
+package app.simplecloud.api.internal.blueprint;
+
+import app.simplecloud.api.CloudApiOptions;
+import app.simplecloud.api.blueprint.Blueprint;
+import app.simplecloud.api.blueprint.BlueprintApi;
+import app.simplecloud.api.blueprint.CreateBlueprintRequest;
+import app.simplecloud.api.blueprint.RuntimeConfig;
+import app.simplecloud.api.blueprint.UpdateBlueprintRequest;
+import app.simplecloud.api.cache.QueryCache;
+import app.simplecloud.api.cache.QueryKey;
+import app.simplecloud.api.web.ApiException;
+import app.simplecloud.api.web.apis.BlueprintsApi;
+import app.simplecloud.api.web.models.ModelsBlueprintSummary;
+import app.simplecloud.api.web.models.ModelsCreateBlueprintRequest;
+import app.simplecloud.api.web.models.ModelsCreateBlueprintResponse;
+import app.simplecloud.api.web.models.ModelsListBlueprintsResponse;
+import app.simplecloud.api.web.models.ModelsRuntimeConfig;
+import app.simplecloud.api.web.models.ModelsUpdateBlueprintRequest;
+import app.simplecloud.api.web.models.V0BlueprintsPostRequest;
+import app.simplecloud.api.web.models.V0BlueprintsPutRequest;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+public class BlueprintApiImpl implements BlueprintApi {
+
+ private final CloudApiOptions options;
+ private final BlueprintsApi blueprintsApi;
+ private final QueryCache cache;
+
+ public BlueprintApiImpl(CloudApiOptions options, QueryCache cache) {
+ this.options = options;
+ this.cache = cache;
+ this.blueprintsApi = new BlueprintsApi();
+ this.blueprintsApi.setCustomBaseUrl(options.getControllerUrl());
+ if (options.getComponent() != null && !options.getComponent().isBlank()) {
+ this.blueprintsApi.getApiClient().addDefaultHeader("X-SC-Component", options.getComponent());
+ }
+ }
+
+ @Override
+ public CompletableFuture getBlueprintById(String id) {
+ QueryKey key = QueryKey.of("blueprint", id);
+
+ return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> {
+ try {
+ ModelsListBlueprintsResponse response = listBlueprints();
+ List blueprints = response.getBlueprints();
+ if (blueprints != null) {
+ for (ModelsBlueprintSummary summary : blueprints) {
+ if (id.equals(summary.getBlueprintId())) {
+ return new BlueprintImpl(summary);
+ }
+ }
+ }
+ throw new RuntimeException("Blueprint not found: " + id);
+ } catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+
+ @Override
+ public CompletableFuture getBlueprintByName(String name) {
+ QueryKey key = QueryKey.of("blueprint", "name", name);
+
+ return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> {
+ try {
+ ModelsListBlueprintsResponse response = listBlueprints();
+ List blueprints = response.getBlueprints();
+ if (blueprints != null) {
+ for (ModelsBlueprintSummary summary : blueprints) {
+ if (name.equals(summary.getName())) {
+ return new BlueprintImpl(summary);
+ }
+ }
+ }
+ throw new RuntimeException("Blueprint not found: " + name);
+ } catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+
+ @Override
+ public CompletableFuture> getAllBlueprints() {
+ QueryKey key = QueryKey.of("blueprints");
+
+ return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> {
+ try {
+ ModelsListBlueprintsResponse response = listBlueprints();
+ List blueprints = response.getBlueprints();
+ if (blueprints == null) {
+ return List.of();
+ }
+
+ return blueprints.stream()
+ .map(BlueprintImpl::new)
+ .toList();
+ } catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+
+ @Override
+ public CompletableFuture createBlueprint(CreateBlueprintRequest request) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ ModelsCreateBlueprintRequest apiRequest = new ModelsCreateBlueprintRequest();
+ apiRequest.setName(request.getName());
+ apiRequest.setConfigurator(request.getConfigurator());
+ apiRequest.setMinecraftVersion(request.getMinecraftVersion());
+ apiRequest.setServerSoftware(request.getServerSoftware());
+ apiRequest.setServerUrl(request.getServerUrl());
+ apiRequest.setSoftwareVersion(request.getSoftwareVersion());
+ apiRequest.setWorkflowSteps(request.getWorkflowSteps());
+ apiRequest.setRuntimeConfig(convertRuntimeConfig(request.getRuntimeConfig()));
+
+ ModelsCreateBlueprintResponse response = blueprintsApi.v0BlueprintsPost(
+ options.getNetworkId(),
+ options.getNetworkSecret(),
+ new V0BlueprintsPostRequest(apiRequest)
+ );
+
+ cache.invalidateAll(QueryKey.of("blueprint"));
+ cache.invalidateAll(QueryKey.of("blueprints"));
+
+ return getBlueprintById(response.getBlueprintId()).join();
+ } catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Override
+ public CompletableFuture updateBlueprint(String id, UpdateBlueprintRequest request) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ ModelsUpdateBlueprintRequest apiRequest = new ModelsUpdateBlueprintRequest();
+ apiRequest.setName(request.getName());
+ apiRequest.setConfigurator(request.getConfigurator());
+ apiRequest.setMinecraftVersion(request.getMinecraftVersion());
+ apiRequest.setServerSoftware(request.getServerSoftware());
+ apiRequest.setServerUrl(request.getServerUrl());
+ apiRequest.setSoftwareVersion(request.getSoftwareVersion());
+ apiRequest.setWorkflowSteps(request.getWorkflowSteps());
+ apiRequest.setRuntimeConfig(convertRuntimeConfig(request.getRuntimeConfig()));
+
+ blueprintsApi.v0BlueprintsPut(
+ options.getNetworkId(),
+ options.getNetworkSecret(),
+ id,
+ new V0BlueprintsPutRequest(apiRequest)
+ );
+
+ cache.invalidateAll(QueryKey.of("blueprint"));
+ cache.invalidateAll(QueryKey.of("blueprints"));
+
+ return getBlueprintById(id).join();
+ } catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Override
+ public CompletableFuture deleteBlueprint(String id) {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ blueprintsApi.v0BlueprintsDelete(
+ options.getNetworkId(),
+ options.getNetworkSecret(),
+ id
+ );
+
+ cache.invalidateAll(QueryKey.of("blueprint"));
+ cache.invalidateAll(QueryKey.of("blueprints"));
+ } catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ private ModelsListBlueprintsResponse listBlueprints() throws ApiException {
+ return blueprintsApi.v0BlueprintsGet(
+ options.getNetworkId(),
+ options.getNetworkSecret()
+ );
+ }
+
+ private ModelsRuntimeConfig convertRuntimeConfig(RuntimeConfig runtimeConfig) {
+ if (runtimeConfig == null) {
+ return null;
+ }
+
+ ModelsRuntimeConfig result = new ModelsRuntimeConfig();
+ if (runtimeConfig.getType() != null) {
+ result.setType(runtimeConfig.getType().name());
+ }
+ result.setWith(runtimeConfig.getWith());
+ return result;
+ }
+}
diff --git a/api/src/main/java/app/simplecloud/api/internal/cache/CacheEventListener.java b/api/src/main/java/app/simplecloud/api/internal/cache/CacheEventListener.java
index 1505c9f..030e702 100644
--- a/api/src/main/java/app/simplecloud/api/internal/cache/CacheEventListener.java
+++ b/api/src/main/java/app/simplecloud/api/internal/cache/CacheEventListener.java
@@ -133,6 +133,22 @@ private void setupListeners(EventApi eventApi) {
schedulePatternInvalidation(QueryKey.of("groups"));
}));
+ // Blueprint events
+ subscriptions.add(eventApi.blueprint().onCreated(event -> {
+ schedulePatternInvalidation(QueryKey.of("blueprint"));
+ schedulePatternInvalidation(QueryKey.of("blueprints"));
+ }));
+
+ subscriptions.add(eventApi.blueprint().onUpdated(event -> {
+ schedulePatternInvalidation(QueryKey.of("blueprint"));
+ schedulePatternInvalidation(QueryKey.of("blueprints"));
+ }));
+
+ subscriptions.add(eventApi.blueprint().onDeleted(event -> {
+ schedulePatternInvalidation(QueryKey.of("blueprint"));
+ schedulePatternInvalidation(QueryKey.of("blueprints"));
+ }));
+
// Persistent server events
subscriptions.add(eventApi.persistentServer().onCreated(event -> {
schedulePatternInvalidation(QueryKey.of("persistentServers"));
diff --git a/api/src/main/java/app/simplecloud/api/internal/server/ServerApiImpl.java b/api/src/main/java/app/simplecloud/api/internal/server/ServerApiImpl.java
index e346a0d..428e3c8 100644
--- a/api/src/main/java/app/simplecloud/api/internal/server/ServerApiImpl.java
+++ b/api/src/main/java/app/simplecloud/api/internal/server/ServerApiImpl.java
@@ -3,6 +3,8 @@
import app.simplecloud.api.CloudApiOptions;
import app.simplecloud.api.cache.QueryCache;
import app.simplecloud.api.cache.QueryKey;
+import app.simplecloud.api.group.Group;
+import app.simplecloud.api.persistentserver.PersistentServer;
import app.simplecloud.api.server.Server;
import app.simplecloud.api.server.ServerApi;
import app.simplecloud.api.server.ServerQuery;
@@ -69,13 +71,46 @@ public CompletableFuture getServerById(String id) {
}
@Override
- public CompletableFuture getServerByNumericalId(String groupName, int numericalId) {
- QueryKey key = QueryKey.of("server", "numerical", groupName, numericalId);
+ public CompletableFuture getServerByName(String serverName, char splitChar) {
+ ParsedServerName parsedServerName = parseServerName(serverName, splitChar);
+ if (parsedServerName == null) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ QueryKey key = QueryKey.of("server", "serverName", serverName, splitChar);
return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> {
try {
ServerQuery query = ServerQuery.create()
- .filterByServerGroupName(groupName)
+ .filterByServerBaseName(parsedServerName.fullName(), parsedServerName.serverBaseName())
+ .filterByNumericalId(-1, parsedServerName.numericalId());
+ ModelsListServersResponse serversResponse = executeQuery(query);
+
+ List servers = serversResponse.getServers();
+ if (servers == null || servers.isEmpty()) {
+ throw new RuntimeException("Server not found: " + serverName);
+ }
+
+ Server server = findServerByName(servers, parsedServerName);
+ if (server != null) {
+ return server;
+ }
+
+ throw new RuntimeException("Server not found: " + serverName);
+ } catch (ApiException e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+
+ @Override
+ public CompletableFuture getServerByNumericalId(String serverBaseName, int numericalId) {
+ QueryKey key = QueryKey.of("server", "numerical", "serverBaseName", serverBaseName, numericalId);
+
+ return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> {
+ try {
+ ServerQuery query = ServerQuery.create()
+ .filterByServerBaseName(serverBaseName)
.filterByNumericalId(numericalId);
ModelsListServersResponse serversResponse = executeQuery(query);
@@ -89,7 +124,7 @@ public CompletableFuture getServerByNumericalId(String groupName, int nu
return new ServerImpl(summary);
}
}
- throw new RuntimeException("Server not found in group " + groupName + " with numerical ID " + numericalId);
+ throw new RuntimeException("Server not found for server base name " + serverBaseName + " with numerical ID " + numericalId);
} catch (ApiException e) {
throw new RuntimeException(e);
@@ -98,13 +133,13 @@ public CompletableFuture getServerByNumericalId(String groupName, int nu
}
@Override
- public CompletableFuture> getServersByGroup(String groupName) {
- QueryKey key = QueryKey.of("servers", "group", groupName);
+ public CompletableFuture> getServersByServerBaseName(String serverBaseName) {
+ QueryKey key = QueryKey.of("servers", "serverBaseName", serverBaseName);
return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> {
try {
ServerQuery query = ServerQuery.create()
- .filterByServerGroupName(groupName);
+ .filterByServerBaseName(serverBaseName);
ModelsListServersResponse serversResponse = executeQuery(query);
List servers = serversResponse.getServers();
@@ -154,7 +189,7 @@ private QueryKey buildQueryKey(@Nullable ServerQuery query) {
query.getServerhostId(),
query.getPersistentServerId(),
query.getServerGroupTypes(),
- query.getServerGroupNames(),
+ query.getServerBaseNames(),
query.getServerGroupTags(),
query.getNumericalIds(),
query.getSortBy(),
@@ -185,9 +220,9 @@ private ModelsListServersResponse executeQuery(@Nullable ServerQuery query) thro
.collect(java.util.stream.Collectors.joining(","));
}
- String serverGroupName = null;
- if (query != null && query.getServerGroupNames() != null && !query.getServerGroupNames().isEmpty()) {
- serverGroupName = String.join(",", query.getServerGroupNames());
+ String serverBaseName = null;
+ if (query != null && query.getServerBaseNames() != null && !query.getServerBaseNames().isEmpty()) {
+ serverBaseName = String.join(",", query.getServerBaseNames());
}
String serverGroupTags = null;
@@ -211,7 +246,7 @@ private ModelsListServersResponse executeQuery(@Nullable ServerQuery query) thro
serverhostId,
persistentServerId,
serverGroupType,
- serverGroupName,
+ serverBaseName,
serverGroupTags,
numericalIds,
sortBy,
@@ -344,4 +379,73 @@ public CompletableFuture