feature(subsystems): extract DiscordRPCSubSystem (#4233)

* docs(subsystems): add Readme for DiscordRPC subsystem
* feature(subsystems): add separate configuration for subsystems and add it to classpath
* feature(subsystems): add subsystems to dist
* feature(subsystems): add  README for subsystems
* chore(subsystems): Use logger.info for subsystems. use common.gradle instead publish.gradle
* feature(subsystems): re-integrate afk subsystem handling in discord submodule

Co-authored-by: Tobias Nett <skaldarnar@googlemail.com>
develop
Nail Khanipov 2020-11-14 23:07:36 +03:00 committed by GitHub
parent 4de0df937b
commit 04572ef4e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 207 additions and 128 deletions

View File

@ -85,7 +85,6 @@ dependencies {
implementation group: 'io.netty', name: 'netty', version: '3.10.5.Final'
implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '2.6.1'
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.2'
// Javax for protobuf due to @Generated - needed on Java 9 or newer Javas
// TODO: Can likely replace with protobuf Gradle task and omit the generated source files instead
implementation group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2'
@ -133,9 +132,6 @@ dependencies {
}
implementation group: 'net.logstash.logback', name: 'logstash-logback-encoder', version: '4.10'
// Discord RPC
api 'com.jagrosh:DiscordIPC:0.4'
// Our developed libs
api group: 'org.terasology', name: 'gestalt-module', version: '5.1.5'
api group: 'org.terasology', name: 'gestalt-util', version: '5.1.5'

View File

@ -16,15 +16,18 @@
package org.terasology.config;
import java.util.List;
import org.terasology.engine.subsystem.rpc.DiscordRPCSubSystem;
import org.terasology.nui.Color;
import org.terasology.rendering.nui.layers.mainMenu.settings.CieCamColors;
import org.terasology.utilities.random.FastRandom;
import org.terasology.utilities.random.Random;
import org.terasology.utilities.subscribables.AbstractSubscribable;
public class PlayerConfig {
import java.util.List;
public class PlayerConfig extends AbstractSubscribable {
public static final String DISCORD_PRESENCE = "DISCORD_PRESENCE";
public static final String PLAYER_NAME = "PLAYER_NAME";
private static final float DEFAULT_PLAYER_HEIGHT = 1.8f;
@ -49,7 +52,9 @@ public class PlayerConfig {
}
public void setName(String name) {
String oldName = this.name;
this.name = name;
propertyChangeSupport.firePropertyChange(PLAYER_NAME, oldName, name);
}
public Color getColor() {
@ -87,14 +92,9 @@ public class PlayerConfig {
}
public void setDiscordPresence(boolean discordPresence) {
boolean oldValue = this.discordPresence;
this.discordPresence = discordPresence;
if (DiscordRPCSubSystem.isEnabled() != discordPresence) {
if (discordPresence) {
DiscordRPCSubSystem.enable();
} else {
DiscordRPCSubSystem.disable();
}
}
propertyChangeSupport.firePropertyChange(DISCORD_PRESENCE, oldValue, discordPresence);
}
public boolean isDiscordPresence() {

View File

@ -1,69 +0,0 @@
/*
* Copyright 2018 MovingBlocks
*
* 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 org.terasology.entitySystem.systems;
import org.terasology.engine.subsystem.rpc.DiscordRPCSubSystem;
import org.terasology.game.Game;
import org.terasology.network.NetworkMode;
import org.terasology.network.NetworkSystem;
import org.terasology.registry.In;
/**
* It's a system that runs when a single player or multi player game has been started to process some stuff
* throw the {@link DiscordRPCSubSystem}.
*
* @see DiscordRPCSubSystem
*/
@RegisterSystem(RegisterMode.CLIENT)
public class DiscordRPCSystem extends BaseComponentSystem {
@In
private Game game;
@In
private NetworkSystem networkSystem;
public String getGame() {
NetworkMode networkMode = networkSystem.getMode();
String mode = "Playing Online";
if (networkMode == NetworkMode.DEDICATED_SERVER) {
mode = "Hosting | " + game.getName();
} else if (networkMode == NetworkMode.NONE) {
mode = "Solo | " + game.getName();
}
return mode;
}
@Override
public void initialise() {
DiscordRPCSubSystem.tryToDiscover();
}
@Override
public void preBegin() {
DiscordRPCSubSystem.setState(getGame(), false);
}
@Override
public void postBegin() {
DiscordRPCSubSystem.setState(getGame(), true);
}
@Override
public void shutdown() {
DiscordRPCSubSystem.setState("In Lobby");
}
}

View File

@ -4,7 +4,6 @@ package org.terasology.logic.afk;
import org.terasology.assets.ResourceUrn;
import org.terasology.engine.Time;
import org.terasology.engine.subsystem.rpc.DiscordRPCSubSystem;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.event.EventPriority;
import org.terasology.entitySystem.event.ReceiveEvent;
@ -79,11 +78,9 @@ public class AfkClientSystem extends BaseComponentSystem {
if (component.afk) {
nuiManager.pushScreen(SCREEN_URL, AfkScreen.class).setAfkClientSystem(this);
nuiManager.closeScreen(CONSOLE_SCREEN_URL);
enableDiscord();
console.addMessage("[AFK] You are AFK now!");
} else {
nuiManager.closeScreen(SCREEN_URL);
disableDiscord();
console.addMessage("[AFK] You are no longer AFK!");
}
AfkRequest request = new AfkRequest(entity, component.afk);
@ -99,7 +96,6 @@ public class AfkClientSystem extends BaseComponentSystem {
if (!component.afk) {
component.afk = true;
nuiManager.pushScreen(SCREEN_URL, AfkScreen.class).setAfkClientSystem(this);
enableDiscord();
AfkRequest request = new AfkRequest(entity, true);
entity.send(request);
}
@ -154,29 +150,11 @@ public class AfkClientSystem extends BaseComponentSystem {
return lastActive;
}
private String getGame() {
NetworkMode networkMode = networkSystem.getMode();
String mode = "Playing Online";
if (networkMode == NetworkMode.DEDICATED_SERVER) {
mode = "Hosting | " + game.getName();
}
return mode;
}
private void updateActive() {
lastActive = time.getGameTimeInMs();
}
private void enableDiscord() {
DiscordRPCSubSystem.tryToDiscover();
DiscordRPCSubSystem.setState("Idle", true);
}
private void disableDiscord() {
DiscordRPCSubSystem.tryToDiscover();
DiscordRPCSubSystem.setState(getGame(), true);
}
private boolean disable() {
EntityRef clientEntity = localPlayer.getClientEntity();
AfkComponent component = clientEntity.getComponent(AfkComponent.class);
@ -186,7 +164,6 @@ public class AfkClientSystem extends BaseComponentSystem {
clientEntity.addOrSaveComponent(component);
AfkRequest request = new AfkRequest(clientEntity, false);
clientEntity.send(request);
disableDiscord();
return true;
}
return false;

View File

@ -19,7 +19,6 @@ import com.google.common.base.Charsets;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.assets.ResourceUrn;

View File

@ -20,12 +20,18 @@ import com.google.common.base.Functions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.math.DoubleMath;
import org.terasology.assets.ResourceUrn;
import org.terasology.config.Config;
import org.terasology.context.Context;
import org.terasology.engine.subsystem.rpc.DiscordRPCSubSystem;
import org.terasology.engine.SimpleUri;
import org.terasology.i18n.TranslationProject;
import org.terasology.i18n.TranslationSystem;
import org.terasology.identity.storageServiceClient.StorageServiceWorker;
import org.terasology.identity.storageServiceClient.StorageServiceWorkerStatus;
import org.terasology.rendering.nui.layers.mainMenu.StorageServiceLoginPopup;
import org.terasology.rendering.nui.layers.mainMenu.ThreeButtonPopup;
import org.terasology.nui.Color;
import org.terasology.nui.WidgetUtil;
import org.terasology.nui.databinding.DefaultBinding;
import org.terasology.nui.databinding.ReadOnlyBinding;
import org.terasology.nui.widgets.UIButton;
import org.terasology.nui.widgets.UICheckbox;
import org.terasology.nui.widgets.UIDropdownScrollable;
@ -33,27 +39,20 @@ import org.terasology.nui.widgets.UIImage;
import org.terasology.nui.widgets.UILabel;
import org.terasology.nui.widgets.UISlider;
import org.terasology.nui.widgets.UIText;
import org.terasology.utilities.Assets;
import org.terasology.assets.ResourceUrn;
import org.terasology.config.Config;
import org.terasology.engine.SimpleUri;
import org.terasology.i18n.TranslationProject;
import org.terasology.i18n.TranslationSystem;
import org.terasology.registry.In;
import org.terasology.rendering.assets.texture.Texture;
import org.terasology.rendering.assets.texture.TextureUtil;
import org.terasology.nui.Color;
import org.terasology.rendering.nui.CoreScreenLayer;
import org.terasology.nui.WidgetUtil;
import org.terasology.rendering.nui.animation.MenuAnimationSystems;
import org.terasology.nui.databinding.DefaultBinding;
import org.terasology.nui.databinding.ReadOnlyBinding;
import org.terasology.rendering.nui.layers.mainMenu.StorageServiceLoginPopup;
import org.terasology.rendering.nui.layers.mainMenu.ThreeButtonPopup;
import org.terasology.utilities.Assets;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Collections;
import static org.terasology.identity.storageServiceClient.StatusMessageTranslator.getLocalizedButtonMessage;
import static org.terasology.identity.storageServiceClient.StatusMessageTranslator.getLocalizedStatusMessage;
@ -326,7 +325,6 @@ public class PlayerSettingsScreen extends CoreScreenLayer {
if (nametext != null) {
config.getPlayer().setName(nametext.getText().trim());
config.getPlayer().setHasEnteredUsername(true);
DiscordRPCSubSystem.updateState();
}
if (!config.getSystem().getLocale().equals(language.getSelection())) {
config.getSystem().setLocale(language.getSelection());

View File

@ -56,7 +56,7 @@ val rootDirDist = File(rootDir, "build/distributions")
// Inherited props
val dirNatives: String by rootProject.extra
val distsDirectory: DirectoryProperty by project;
val distsDirectory: DirectoryProperty by project
// Read environment variables, including variables passed by jenkins continuous integration server
val env: MutableMap<String, String> = System.getenv()!!
@ -88,6 +88,7 @@ group = "org.terasology.facades"
dependencies {
implementation(project(":engine"))
implementation(group = "org.reflections", name = "reflections", version = "0.9.10")
implementation(project(":subsystems:DiscordRPC"))
// TODO: Consider whether we can move the CR dependency back here from the engine, where it is referenced from the main menu
implementation(group = "org.terasology.crashreporter", name = "cr-terasology", version = "4.1.0")
@ -171,7 +172,7 @@ tasks.register<Sync>("setupServerModules") {
description =
"""Parses "extraModules" - a comma-separated list of modules and puts them into $localServerDataPath"""
val extraModules: String? by project;
val extraModules: String? by project
extraModules?.let {
// Grab modules from Artifactory - cheats by declaring them as dependencies
it.splitToSequence(",").forEach {

View File

@ -18,6 +18,7 @@ import org.terasology.engine.subsystem.common.ConfigurationSubsystem;
import org.terasology.engine.subsystem.common.ThreadManager;
import org.terasology.engine.subsystem.common.hibernation.HibernationSubsystem;
import org.terasology.engine.subsystem.config.BindsSubsystem;
import org.terasology.engine.subsystem.discordrpc.DiscordRPCSubSystem;
import org.terasology.engine.subsystem.headless.HeadlessAudio;
import org.terasology.engine.subsystem.headless.HeadlessGraphics;
import org.terasology.engine.subsystem.headless.HeadlessInput;
@ -29,7 +30,6 @@ import org.terasology.engine.subsystem.lwjgl.LwjglGraphics;
import org.terasology.engine.subsystem.lwjgl.LwjglInput;
import org.terasology.engine.subsystem.lwjgl.LwjglTimer;
import org.terasology.engine.subsystem.openvr.OpenVRInput;
import org.terasology.engine.subsystem.rpc.DiscordRPCSubSystem;
import org.terasology.game.GameManifest;
import org.terasology.network.NetworkMode;
import org.terasology.rendering.nui.layers.mainMenu.savedGames.GameInfo;

View File

@ -0,0 +1,10 @@
# DiscordRPC Subsystem
The Discord RPC subsystem (rich presence via IPC) allows to set and update the status message in Discord for Terasology.
When this system is active and the player has the Discord desktop client open the user status will be shown as "Playing Terasology" (or something like that).
The subsystem uses https://github.com/jagrosh/DiscordIPC (`com.jagrosh:DiscordIPC`) to communicate with the Discord client.
## Requirements
This subsystem only has an effect if the user has the [Discord Desktop client](https://discord.com/) and is logged in.

View File

@ -0,0 +1,15 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
plugins {
java
`java-library`
}
apply(from = "$rootDir/config/gradle/common.gradle")
dependencies {
implementation(project(":engine"))
api("com.jagrosh:DiscordIPC:0.4")
implementation("ch.qos.logback:logback-classic:1.2.3")
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.engine.subsystem.rpc;
package org.terasology.engine.subsystem.discordrpc;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
@ -25,10 +25,13 @@ import com.jagrosh.discordipc.entities.pipe.WindowsPipe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.config.Config;
import org.terasology.config.PlayerConfig;
import org.terasology.context.Context;
import org.terasology.engine.GameEngine;
import org.terasology.engine.subsystem.EngineSubsystem;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.time.OffsetDateTime;
/**
@ -37,7 +40,7 @@ import java.time.OffsetDateTime;
*
* @see EngineSubsystem
*/
public class DiscordRPCSubSystem implements EngineSubsystem, IPCListener, Runnable {
public class DiscordRPCSubSystem implements EngineSubsystem, IPCListener, Runnable, PropertyChangeListener {
private static final Logger logger = LoggerFactory.getLogger(DiscordRPCSubSystem.class);
private static final long DISCORD_APP_CLIENT_ID = 515274721080639504L;
@ -193,6 +196,7 @@ public class DiscordRPCSubSystem implements EngineSubsystem, IPCListener, Runnab
@Override
public void postInitialise(Context context) {
config = context.get(Config.class);
config.getPlayer().subscribe(this);
setState("In Lobby");
}
@ -295,4 +299,20 @@ public class DiscordRPCSubSystem implements EngineSubsystem, IPCListener, Runnab
return getInstance().enabled;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(PlayerConfig.DISCORD_PRESENCE)) {
boolean discordPresence = (boolean) evt.getNewValue();
if (isEnabled() != discordPresence) {
if (discordPresence) {
enable();
} else {
disable();
}
}
}
if (evt.getPropertyName().equals(PlayerConfig.PLAYER_NAME)) {
updateState();
}
}
}

View File

@ -0,0 +1,92 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.engine.subsystem.discordrpc;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.event.ReceiveEvent;
import org.terasology.entitySystem.systems.BaseComponentSystem;
import org.terasology.entitySystem.systems.RegisterMode;
import org.terasology.entitySystem.systems.RegisterSystem;
import org.terasology.game.Game;
import org.terasology.logic.afk.AfkEvent;
import org.terasology.logic.players.LocalPlayer;
import org.terasology.network.NetworkMode;
import org.terasology.network.NetworkSystem;
import org.terasology.registry.In;
/**
* It's a system that runs when a single player or multi player game has been started to process some stuff throw the
* {@link DiscordRPCSubSystem}.
*
* @see DiscordRPCSubSystem
*/
@RegisterSystem(RegisterMode.CLIENT)
public class DiscordRPCSystem extends BaseComponentSystem {
@In
private Game game;
@In
private LocalPlayer player;
@In
private NetworkSystem networkSystem;
public String getGame() {
NetworkMode networkMode = networkSystem.getMode();
String mode = "Playing Online";
if (networkMode == NetworkMode.DEDICATED_SERVER) {
mode = "Hosting | " + game.getName();
} else if (networkMode == NetworkMode.NONE) {
mode = "Solo | " + game.getName();
}
return mode;
}
@ReceiveEvent
public void onAfk(AfkEvent event, EntityRef entityRef) {
if (requireConnection() && player.getClientEntity().equals(entityRef)) {
return;
}
if (event.isAfk()) {
disableDiscord();
} else {
enableDiscord();
}
}
private boolean requireConnection() {
NetworkMode networkMode = networkSystem.getMode();
return networkMode != NetworkMode.CLIENT && networkMode != NetworkMode.DEDICATED_SERVER;
}
private void enableDiscord() {
DiscordRPCSubSystem.tryToDiscover();
DiscordRPCSubSystem.setState("Idle", true);
}
private void disableDiscord() {
DiscordRPCSubSystem.tryToDiscover();
DiscordRPCSubSystem.setState(getGame(), true);
}
@Override
public void initialise() {
DiscordRPCSubSystem.tryToDiscover();
}
@Override
public void preBegin() {
DiscordRPCSubSystem.setState(getGame(), false);
}
@Override
public void postBegin() {
DiscordRPCSubSystem.setState(getGame(), true);
}
@Override
public void shutdown() {
DiscordRPCSubSystem.setState("In Lobby");
}
}

24
subsystems/README.MD Normal file
View File

@ -0,0 +1,24 @@
# Subsystems
Subsystems provide and extend engine functionality that might not be needed all the time.
A subsystem can provide an API to be implemented by other subsystems (API subsystem). Such an API subsystem does not provide any functionality on its own.
Typical examples for subsystems are:
* platform integration (e.g., Discord)
* native libraries usage
* network activity
> :warning: **Subsystems should not extend or provide gameplay features!** Use Modules instead.
>
> It is planned to allow combinations of modules with subsystems for new functionality in the future.
### Comparation between Module and Subsystem functionality:
| | Module | Subsystem
--------------|---------------------------------------------------|----------
Boot | at game start | at game launch
Sandbox | Yes | No
Installing | Yes, in-game download from server or repository | No, with facade or engine only
Dependencies | only another Modules | any, except Modules
Build Script | engine-driven | free-style

View File

@ -0,0 +1,16 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
// This magically allows subdirs in this subproject to themselves become sub-subprojects in a proper tree structure
new File(rootDir, 'subsystems').eachDir { possibleSubprojectDir ->
if (!possibleSubprojectDir.name.startsWith(".")) {
def subprojectName = 'subsystems:' + possibleSubprojectDir.name
logger.info("Including '$subprojectName' as a sub-project")
include subprojectName
def subprojectPath = ':' + subprojectName
def subproject = project(subprojectPath)
subproject.projectDir = possibleSubprojectDir
} else {
logger.info("Ignoring hidden folder '$possibleSubprojectDir'")
}
}