eu.cloudnetservice.modules.bridge.BridgeServiceHelper Maven / Gradle / Ivy
Show all versions of bridge Show documentation
/*
* Copyright 2019-2024 CloudNetService team & contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.cloudnetservice.modules.bridge;
import eu.cloudnetservice.common.resource.ResourceFormatter;
import eu.cloudnetservice.common.util.StringUtil;
import eu.cloudnetservice.driver.provider.CloudServiceFactory;
import eu.cloudnetservice.driver.provider.ServiceTaskProvider;
import eu.cloudnetservice.driver.service.ServiceConfiguration;
import eu.cloudnetservice.driver.service.ServiceCreateResult;
import eu.cloudnetservice.driver.service.ServiceInfoSnapshot;
import eu.cloudnetservice.driver.service.ServiceLifeCycle;
import eu.cloudnetservice.wrapper.configuration.WrapperConfiguration;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
/**
* The bridge service helper is designed to facilitate working with individual states of services. In addition, these
* properties are stored here:
*
* - {@link #motd()}
* - {@link #extra()}
* - {@link #state()}
* - {@link #maxPlayers()}
*
*
* @since 4.0
*/
@Singleton
public final class BridgeServiceHelper {
private final AtomicInteger maxPlayers = new AtomicInteger();
private final AtomicReference motd = new AtomicReference<>("");
private final AtomicReference extra = new AtomicReference<>("");
private final AtomicReference state = new AtomicReference<>("LOBBY");
private final ServiceTaskProvider taskProvider;
private final CloudServiceFactory serviceFactory;
private final WrapperConfiguration wrapperConfiguration;
@Inject
public BridgeServiceHelper(
@NonNull ServiceTaskProvider taskProvider,
@NonNull CloudServiceFactory serviceFactory,
@NonNull WrapperConfiguration wrapperConfiguration
) {
this.taskProvider = taskProvider;
this.serviceFactory = serviceFactory;
this.wrapperConfiguration = wrapperConfiguration;
}
/**
* Tries to guess the {@link ServiceInfoState} from the lifecycle and {@link BridgeDocProperties} of the given
* service.
*
* - If the service is not running or not in-game {@link ServiceInfoState#STOPPED} is guessed.
* - If the service is empty {@link ServiceInfoState#EMPTY_ONLINE} is guessed.
* - If the service is full {@link ServiceInfoState#FULL_ONLINE} is guessed.
* - If the service is starting {@link ServiceInfoState#STARTING} is guessed.
* - If the service is just connected {@link ServiceInfoState#ONLINE} is guessed.
* - If none of the above apply {@link ServiceInfoState#STOPPED} is used as fallback.
*
*
* @param service the service to guess the state of.
* @return the guessed service info state.
* @throws NullPointerException if the given service is null.
* @see BridgeDocProperties
*/
public static @NonNull ServiceInfoState guessStateFromServiceInfoSnapshot(@NonNull ServiceInfoSnapshot service) {
// convert not running or ingame services to STOPPED
if (service.lifeCycle() != ServiceLifeCycle.RUNNING || inGameService(service)) {
return ServiceInfoState.STOPPED;
}
// check if the service is empty
if (emptyService(service)) {
return ServiceInfoState.EMPTY_ONLINE;
}
// check if the service is full
if (fullService(service)) {
return ServiceInfoState.FULL_ONLINE;
}
// check if the service is starting
if (startingService(service)) {
return ServiceInfoState.STARTING;
}
// check if the service is connected
if (service.connected()) {
return ServiceInfoState.ONLINE;
}
return ServiceInfoState.STOPPED;
}
/**
* Replaces commonly used placeholders in the given input string using the given service as the information source. If
* no service is given only the group property is replaced.
*
* @param value the string to replace the placeholders in.
* @param group the group to replace {@literal %group%} with.
* @param service the service to use as source for the placeholder values.
* @return the String with the placeholders replaced.
* @throws NullPointerException if the given input string is null.
*/
public static @NonNull String fillCommonPlaceholders(
@NonNull String value,
@Nullable String group,
@Nullable ServiceInfoSnapshot service
) {
value = value.replace("%group%", group == null ? "" : group);
// stop replacing if no service is given
if (service == null) {
return value;
}
// replace all service id placeholders
value = value.replace("%name%", service.serviceId().name());
value = value.replace("%task%", service.serviceId().taskName());
value = value.replace("%node%", service.serviceId().nodeUniqueId());
value = value.replace("%unique_id%", service.serviceId().uniqueId().toString());
value = value.replace("%environment%", service.serviceId().environment().name());
value = value.replace("%task_id%", Integer.toString(service.serviceId().taskServiceId()));
value = value.replace("%uid%", service.serviceId().uniqueId().toString().split("-")[0]);
// general service information
value = value.replace("%life_cycle%", service.lifeCycle().name());
value = value.replace("%runtime%", service.configuration().runtime());
value = value.replace("%port%", Integer.toString(service.configuration().port()));
// process information
value = value.replace("%pid%", Long.toString(service.processSnapshot().pid()));
value = value.replace("%threads%", Integer.toString(service.processSnapshot().threads().size()));
value = value.replace("%heap_usage%", Long.toString(service.processSnapshot().heapUsageMemory()));
value = value.replace("%max_heap_usage%", Long.toString(service.processSnapshot().maxHeapMemory()));
value = value.replace(
"%cpu_usage%",
ResourceFormatter.formatTwoDigitPrecision(service.processSnapshot().cpuUsage()));
// bridge information
var online = service.readProperty(BridgeDocProperties.IS_ONLINE);
value = value.replace("%online%", online ? "Online" : "Offline");
// make sure that the bridge is loaded before accessing any of the properties
if (online) {
value = value.replace(
"%online_players%",
Integer.toString(service.readProperty(BridgeDocProperties.ONLINE_COUNT)));
value = value.replace(
"%max_players%",
Integer.toString(service.readProperty(BridgeDocProperties.MAX_PLAYERS)));
value = value.replace("%motd%", service.readProperty(BridgeDocProperties.MOTD));
value = value.replace("%extra%", service.readProperty(BridgeDocProperties.EXTRA));
value = value.replace("%state%", service.readProperty(BridgeDocProperties.STATE));
value = value.replace("%version%", service.readProperty(BridgeDocProperties.VERSION));
}
// done
return value;
}
/**
* Checks if the given service is empty. This is only the case if all following conditions apply:
*
* - the lifecycle is running
* - the service is connected
* - the service is marked as online {@link BridgeDocProperties#IS_ONLINE}
* - the service has a present online count as it is 0
*
*
* @param service the service to check.
* @return true if the service is empty, false otherwise.
* @throws NullPointerException if the given service is null.
*/
public static boolean emptyService(@NonNull ServiceInfoSnapshot service) {
return service.connected()
&& service.readProperty(BridgeDocProperties.IS_ONLINE)
&& service.readProperty(BridgeDocProperties.ONLINE_COUNT) == 0;
}
/**
* Checks if the given service is full. This is only the case if all following conditions apply:
*
* - the lifecycle is running
* - the service is connected
* - the service is marked as online {@link BridgeDocProperties#IS_ONLINE}
* - the service has both an online and max player count
* - the online count is equal or higher than the max player count
*
*
* @param service the service to check.
* @return true if the service is full, false otherwise.
* @throws NullPointerException if the given service is null.
*/
public static boolean fullService(@NonNull ServiceInfoSnapshot service) {
return service.connected()
&& service.readProperty(BridgeDocProperties.IS_ONLINE)
&& service.readProperty(BridgeDocProperties.ONLINE_COUNT)
>= service.readProperty(BridgeDocProperties.MAX_PLAYERS);
}
/**
* Checks if the given service is starting. This is only the case if all following conditions apply:
*
* - the lifecycle is running
* - the service is NOT marked as online {@link BridgeDocProperties#IS_ONLINE}
*
*
* @param service the service to check.
* @return true if the service is starting, false otherwise.
* @throws NullPointerException if the given service is null.
*/
public static boolean startingService(@NonNull ServiceInfoSnapshot service) {
return service.lifeCycle() == ServiceLifeCycle.RUNNING && !service.readProperty(BridgeDocProperties.IS_ONLINE);
}
/**
* Checks if the given service is in-game. This is only the case if all following conditions apply:
*
* - the lifecycle is running
* - the service is connected
* - the service is marked as online {@link BridgeDocProperties#IS_ONLINE}
* - the motd, the state or the extra matches any value representing the in-game state {@link #matchesInGameString(String)}
*
*
* @param service the service to check.
* @return true if the service is in-game, false otherwise.
* @throws NullPointerException if the given service is null.
*/
public static boolean inGameService(@NonNull ServiceInfoSnapshot service) {
return service.lifeCycle() == ServiceLifeCycle.RUNNING
&& service.connected()
&& service.readProperty(BridgeDocProperties.IS_ONLINE)
&& (matchesInGameString(service.readProperty(BridgeDocProperties.MOTD))
|| matchesInGameString(service.readProperty(BridgeDocProperties.EXTRA))
|| matchesInGameString(service.readProperty(BridgeDocProperties.STATE)));
}
/**
* Checks if the given value matches either {@code ingame}, {@code running} or {@code playing}.
*
* @param value the value to check against.
* @return true if the value contains an in-game indicator, false otherwise.
*/
private static boolean matchesInGameString(@Nullable String value) {
if (value == null) {
// value is not present
return false;
} else {
// value is present, check if the string contains one of the ingame string values
var loweredValue = StringUtil.toLower(value);
return loweredValue.contains("ingame") || loweredValue.contains("running") || loweredValue.contains("playing");
}
}
/**
* Sets the state of the service this bridge instance is running on to {@code ingame} and starts a new service of the
* task. The method is equivalent to calling {@code BridgeServiceHelper.changeToIngame(true)}.
*/
public void changeToIngame() {
this.changeToIngame(true);
}
public @NonNull AtomicInteger maxPlayers() {
return this.maxPlayers;
}
public @NonNull AtomicReference motd() {
return this.motd;
}
public @NonNull AtomicReference extra() {
return this.extra;
}
public @NonNull AtomicReference state() {
return this.state;
}
/**
* Sets the state of the service this bridge instance is running on to {@code ingame}. A new service from the same
* task is started if the given auto start option is true.
*
* Note: The service is started asynchronously.
*
* @param autoStartService whether a new service should be started or not.
*/
public void changeToIngame(boolean autoStartService) {
if (!this.state.getAndSet("INGAME").equalsIgnoreCase("ingame") && autoStartService) {
// start a new service based on the task name
var taskName = this.wrapperConfiguration.serviceConfiguration().serviceId().taskName();
this.taskProvider
.serviceTaskAsync(taskName)
.thenApply(task -> ServiceConfiguration.builder(task).build())
.thenApply(this.serviceFactory::createCloudService)
.thenAccept(createResult -> {
if (createResult.state() == ServiceCreateResult.State.CREATED) {
createResult.serviceInfo().provider().start();
}
});
}
}
/**
* The service info state allows a better separation of a service state than a service lifecycle.
*
* @since 4.0
*/
public enum ServiceInfoState {
/**
* This state represents a service that is stopped or does not match any other state.
*/
STOPPED,
/**
* This state represents a service that is currently starting.
*/
STARTING,
/**
* This state represents a service that is started and online but no players are connected to it.
*/
EMPTY_ONLINE,
/**
* This state represents a service that is started, online and full.
*/
FULL_ONLINE,
/**
* This state represents a service that is started and online but neither empty nor full.
*/
ONLINE
}
}