Initial commit
This commit is contained in:
commit
d3838f6a78
119 changed files with 6112 additions and 0 deletions
119
.gitignore
vendored
Normal file
119
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Compiled class file
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log file
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# BlueJ files
|
||||||
|
*.ctxt
|
||||||
|
|
||||||
|
# Package Files #
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
|
hs_err_pid*
|
||||||
|
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
|
# Dump file
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Ignore Gradle GUI config
|
||||||
|
gradle-app.setting
|
||||||
|
|
||||||
|
# Cache of project
|
||||||
|
.gradletasknamecache
|
||||||
|
|
||||||
|
**/build/
|
||||||
|
|
||||||
|
# Common working directory
|
||||||
|
run/
|
||||||
|
runs/
|
||||||
|
|
||||||
|
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||||
|
!gradle-wrapper.jar
|
||||||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2025
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
6
build.gradle
Normal file
6
build.gradle
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
plugins {
|
||||||
|
// see https://fabricmc.net/develop/ for new versions
|
||||||
|
id 'fabric-loom' version '1.7-SNAPSHOT' apply false
|
||||||
|
// see https://projects.neoforged.net/neoforged/moddevgradle for new versions
|
||||||
|
id 'net.neoforged.moddev' version '0.1.110' apply false
|
||||||
|
}
|
||||||
3
buildSrc/build.gradle
Normal file
3
buildSrc/build.gradle
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
plugins {
|
||||||
|
id 'groovy-gradle-plugin'
|
||||||
|
}
|
||||||
129
buildSrc/src/main/groovy/multiloader-common.gradle
Normal file
129
buildSrc/src/main/groovy/multiloader-common.gradle
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
id 'maven-publish'
|
||||||
|
}
|
||||||
|
|
||||||
|
base {
|
||||||
|
archivesName = "${mod_id}-${project.name}-${minecraft_version}"
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(java_version)
|
||||||
|
withSourcesJar()
|
||||||
|
withJavadocJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
// https://docs.gradle.org/current/userguide/declaring_repositories.html#declaring_content_exclusively_found_in_one_repository
|
||||||
|
exclusiveContent {
|
||||||
|
forRepository {
|
||||||
|
maven {
|
||||||
|
name = 'Sponge'
|
||||||
|
url = 'https://repo.spongepowered.org/repository/maven-public'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter { includeGroupAndSubgroups('org.spongepowered') }
|
||||||
|
}
|
||||||
|
exclusiveContent {
|
||||||
|
forRepositories(maven {
|
||||||
|
name = 'ParchmentMC'
|
||||||
|
url = 'https://maven.parchmentmc.org/'
|
||||||
|
},
|
||||||
|
maven {
|
||||||
|
name = "NeoForge"
|
||||||
|
url = 'https://maven.neoforged.net/releases'
|
||||||
|
})
|
||||||
|
filter { includeGroup('org.parchmentmc.data') }
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = 'BlameJared'
|
||||||
|
url = 'https://maven.blamejared.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
// from solstice 1.7
|
||||||
|
maven { url 'https://maven.nucleoid.xyz' }
|
||||||
|
|
||||||
|
maven {
|
||||||
|
name = "TerraformersMC"
|
||||||
|
url = "https://maven.terraformersmc.com/"
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "Ladysnake Libs"
|
||||||
|
url = 'https://maven.ladysnake.org/releases'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare capabilities on the outgoing configurations.
|
||||||
|
// Read more about capabilities here: https://docs.gradle.org/current/userguide/component_capabilities.html#sec:declaring-additional-capabilities-for-a-local-component
|
||||||
|
['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant ->
|
||||||
|
configurations."$variant".outgoing {
|
||||||
|
capability("$group:${base.archivesName.get()}:$version")
|
||||||
|
capability("$group:$mod_id-${project.name}-${minecraft_version}:$version")
|
||||||
|
capability("$group:$mod_id:$version")
|
||||||
|
}
|
||||||
|
publishing.publications.configureEach {
|
||||||
|
suppressPomMetadataWarningsFor(variant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcesJar {
|
||||||
|
from(rootProject.file('LICENSE')) {
|
||||||
|
rename { "${it}_${mod_name}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
from(rootProject.file('LICENSE')) {
|
||||||
|
rename { "${it}_${mod_name}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest {
|
||||||
|
attributes(['Specification-Title' : mod_name,
|
||||||
|
'Specification-Vendor' : mod_author,
|
||||||
|
'Specification-Version' : project.jar.archiveVersion,
|
||||||
|
'Implementation-Title' : project.name,
|
||||||
|
'Implementation-Version': project.jar.archiveVersion,
|
||||||
|
'Implementation-Vendor' : mod_author,
|
||||||
|
'Built-On-Minecraft' : minecraft_version])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
var expandProps = ['version' : version,
|
||||||
|
'group' : project.group, //Else we target the task's group.
|
||||||
|
'minecraft_version' : minecraft_version,
|
||||||
|
'minecraft_version_range' : minecraft_version_range,
|
||||||
|
'fabric_version' : fabric_version,
|
||||||
|
'fabric_loader_version' : fabric_loader_version,
|
||||||
|
'mod_name' : mod_name,
|
||||||
|
'mod_author' : mod_author,
|
||||||
|
'mod_id' : mod_id,
|
||||||
|
'license' : license,
|
||||||
|
'description' : project.description,
|
||||||
|
'neoforge_version' : neoforge_version,
|
||||||
|
'neoforge_loader_version_range': neoforge_loader_version_range,
|
||||||
|
"forge_version" : forge_version,
|
||||||
|
"forge_loader_version_range" : forge_loader_version_range,
|
||||||
|
'credits' : credits,
|
||||||
|
'java_version' : java_version]
|
||||||
|
|
||||||
|
filesMatching(['pack.mcmeta', 'fabric.mod.json', 'META-INF/mods.toml', 'META-INF/neoforge.mods.toml', '*.mixins.json']) {
|
||||||
|
expand expandProps
|
||||||
|
}
|
||||||
|
inputs.properties(expandProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
register('mavenJava', MavenPublication) {
|
||||||
|
artifactId base.archivesName.get()
|
||||||
|
from components.java
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url System.getenv('local_maven_url')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
buildSrc/src/main/groovy/multiloader-loader.gradle
Normal file
44
buildSrc/src/main/groovy/multiloader-loader.gradle
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
plugins {
|
||||||
|
id 'multiloader-common'
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
commonJava {
|
||||||
|
canBeResolved = true
|
||||||
|
}
|
||||||
|
commonResources {
|
||||||
|
canBeResolved = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(project(':common')) {
|
||||||
|
capabilities {
|
||||||
|
requireCapability "$group:$mod_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commonJava project(path: ':common', configuration: 'commonJava')
|
||||||
|
commonResources project(path: ':common', configuration: 'commonResources')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('compileJava', JavaCompile) {
|
||||||
|
dependsOn(configurations.commonJava)
|
||||||
|
source(configurations.commonJava)
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
dependsOn(configurations.commonResources)
|
||||||
|
from(configurations.commonResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('javadoc', Javadoc).configure {
|
||||||
|
dependsOn(configurations.commonJava)
|
||||||
|
source(configurations.commonJava)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('sourcesJar', Jar) {
|
||||||
|
dependsOn(configurations.commonJava)
|
||||||
|
from(configurations.commonJava)
|
||||||
|
dependsOn(configurations.commonResources)
|
||||||
|
from(configurations.commonResources)
|
||||||
|
}
|
||||||
47
common/build.gradle
Normal file
47
common/build.gradle
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
plugins {
|
||||||
|
id 'multiloader-common'
|
||||||
|
id 'net.neoforged.moddev'
|
||||||
|
}
|
||||||
|
|
||||||
|
neoForge {
|
||||||
|
neoFormVersion = neo_form_version
|
||||||
|
// Automatically enable AccessTransformers if the file exists
|
||||||
|
def at = file('src/main/resources/META-INF/accesstransformer.cfg')
|
||||||
|
if (at.exists()) {
|
||||||
|
accessTransformers.add(at.absolutePath)
|
||||||
|
}
|
||||||
|
parchment {
|
||||||
|
minecraftVersion = parchment_minecraft
|
||||||
|
mappingsVersion = parchment_version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5'
|
||||||
|
// fabric and neoforge both bundle mixinextras, so it is safe to use it in common
|
||||||
|
compileOnly group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.3.5'
|
||||||
|
annotationProcessor group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.3.5'
|
||||||
|
|
||||||
|
implementation("org.spongepowered:configurate-core:${project.configurate_version}")
|
||||||
|
implementation("org.spongepowered:configurate-hocon:${project.configurate_version}")
|
||||||
|
implementation("org.spongepowered:configurate-gson:${project.configurate_version}")
|
||||||
|
implementation("com.typesafe:config:1.4.3")
|
||||||
|
implementation("io.leangen.geantyref:geantyref:1.3.16")
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
commonJava {
|
||||||
|
canBeResolved = false
|
||||||
|
canBeConsumed = true
|
||||||
|
}
|
||||||
|
commonResources {
|
||||||
|
canBeResolved = false
|
||||||
|
canBeConsumed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
artifacts {
|
||||||
|
commonJava sourceSets.main.java.sourceDirectories.singleFile
|
||||||
|
commonResources sourceSets.main.resources.sourceDirectories.singleFile
|
||||||
|
}
|
||||||
|
|
||||||
13
common/src/main/java/me/alexdevs/solstice/Constants.java
Normal file
13
common/src/main/java/me/alexdevs/solstice/Constants.java
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package me.alexdevs.solstice;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class Constants {
|
||||||
|
|
||||||
|
public static final String MOD_ID = "solstice";
|
||||||
|
public static final String MOD_NAME = "Solstice";
|
||||||
|
public static final Logger LOG = LoggerFactory.getLogger(MOD_NAME);
|
||||||
|
}
|
||||||
105
common/src/main/java/me/alexdevs/solstice/Solstice.java
Normal file
105
common/src/main/java/me/alexdevs/solstice/Solstice.java
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
package me.alexdevs.solstice;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.api.data.HoconDataManager;
|
||||||
|
import me.alexdevs.solstice.api.events.SolsticeEvents;
|
||||||
|
import me.alexdevs.solstice.api.events.WorldSaveCallback;
|
||||||
|
import me.alexdevs.solstice.core.*;
|
||||||
|
import me.alexdevs.solstice.data.PlayerDataManager;
|
||||||
|
import me.alexdevs.solstice.data.ServerData;
|
||||||
|
import me.alexdevs.solstice.integrations.LuckPermsIntegration;
|
||||||
|
import me.alexdevs.solstice.locale.LocaleManager;
|
||||||
|
import me.alexdevs.solstice.platform.Services;
|
||||||
|
import me.alexdevs.solstice.platform.services.IPlatformHelper;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
import org.spongepowered.configurate.ConfigurateException;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
public class Solstice {
|
||||||
|
public static final HoconDataManager configManager = new HoconDataManager(Services.PLATFORM.getConfigDir().resolve("config.conf"));
|
||||||
|
public static final LocaleManager localeManager = new LocaleManager(Services.PLATFORM.getConfigDir().resolve("locale.json"));
|
||||||
|
public static final ServerData serverData = new ServerData();
|
||||||
|
public static final PlayerDataManager playerData = new PlayerDataManager();
|
||||||
|
public static final Modules modules = new Modules();
|
||||||
|
private static final ConcurrentLinkedQueue<Runnable> nextTickRunnables = new ConcurrentLinkedQueue<>();
|
||||||
|
public static MinecraftServer server;
|
||||||
|
public static Scheduler scheduler = new Scheduler(1, nextTickRunnables);
|
||||||
|
public static final CooldownManager cooldown = new CooldownManager();
|
||||||
|
public static final WarmUpManager warmUp = new WarmUpManager();
|
||||||
|
private static Solstice INSTANCE;
|
||||||
|
private static final UserCache userCache = new UserCache(Services.PLATFORM.getGameDir().resolve("usercache.json").toFile());
|
||||||
|
|
||||||
|
public Solstice() {
|
||||||
|
INSTANCE = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Solstice getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void nextTick(Runnable runnable) {
|
||||||
|
nextTickRunnables.add(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserCache getUserCache() {
|
||||||
|
return userCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
//var modMeta = FabricLoader.getInstance().getModContainer(MOD_ID).get().getMetadata();
|
||||||
|
Constants.LOG.info("Initializing Solstice v{}...", Services.PLATFORM.getVersion());
|
||||||
|
|
||||||
|
LuckPermsIntegration.register();
|
||||||
|
|
||||||
|
modules.register();
|
||||||
|
modules.initModules();
|
||||||
|
|
||||||
|
ToggleableConfig.get().save();
|
||||||
|
|
||||||
|
try {
|
||||||
|
configManager.prepareData();
|
||||||
|
configManager.save();
|
||||||
|
|
||||||
|
} catch (ConfigurateException e) {
|
||||||
|
Constants.LOG.error("Error while loading Solstice config! Refusing to continue!", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localeManager.load();
|
||||||
|
localeManager.save();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Constants.LOG.error("Error while loading Solstice locale!", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerLifecycleEvents.SERVER_STARTING.register(server -> {
|
||||||
|
Solstice.server = server;
|
||||||
|
var path = server.getWorldPath(LevelResource.ROOT).resolve("data").resolve(MOD_ID);
|
||||||
|
if (!path.toFile().exists()) {
|
||||||
|
path.toFile().mkdirs();
|
||||||
|
}
|
||||||
|
serverData.setDataPath(path.resolve("server.json"));
|
||||||
|
playerData.setDataPath(path.resolve("players"));
|
||||||
|
|
||||||
|
serverData.loadData(false);
|
||||||
|
});
|
||||||
|
ServerLifecycleEvents.SERVER_STARTED.register(server -> SolsticeEvents.READY.invoker().onReady(INSTANCE, server));
|
||||||
|
ServerLifecycleEvents.SERVER_STOPPING.register(server -> scheduler.shutdown());
|
||||||
|
ServerLifecycleEvents.SERVER_STOPPED.register(server -> scheduler.shutdownNow());
|
||||||
|
WorldSaveCallback.EVENT.register((server1, suppressLogs, flush, force) -> {
|
||||||
|
serverData.save();
|
||||||
|
playerData.saveAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerTickEvents.START_SERVER_TICK.register(server -> {
|
||||||
|
nextTickRunnables.forEach(Runnable::run);
|
||||||
|
nextTickRunnables.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcast(Component text) {
|
||||||
|
server.getPlayerList().broadcastSystemMessage(text, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package me.alexdevs.solstice.api;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.Expose;
|
||||||
|
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ConfigSerializable
|
||||||
|
public class PlayerMail {
|
||||||
|
@Expose
|
||||||
|
public String message;
|
||||||
|
@Expose
|
||||||
|
public UUID sender;
|
||||||
|
@Expose
|
||||||
|
public Date date;
|
||||||
|
|
||||||
|
public PlayerMail(String message, UUID sender) {
|
||||||
|
this.message = message;
|
||||||
|
this.sender = sender;
|
||||||
|
this.date = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
51
common/src/main/java/me/alexdevs/solstice/api/Raycast.java
Normal file
51
common/src/main/java/me/alexdevs/solstice/api/Raycast.java
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package me.alexdevs.solstice.api;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.ClipContext;
|
||||||
|
import net.minecraft.world.phys.BlockHitResult;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
public class Raycast {
|
||||||
|
public static BlockHitResult cast(ServerPlayer player, double maxDistance) {
|
||||||
|
var world = player.serverLevel();
|
||||||
|
var eyePos = player.getEyePosition();
|
||||||
|
var rotVec = player.getLookAngle();
|
||||||
|
|
||||||
|
var raycastEnd = eyePos.add(rotVec.scale(maxDistance));
|
||||||
|
|
||||||
|
var rcContext = new ClipContext(
|
||||||
|
eyePos,
|
||||||
|
raycastEnd,
|
||||||
|
ClipContext.Block.OUTLINE,
|
||||||
|
ClipContext.Fluid.NONE,
|
||||||
|
player
|
||||||
|
);
|
||||||
|
|
||||||
|
return world.clip(rcContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BlockPos getBlockPos(ServerPlayer player, double maxDistance) {
|
||||||
|
var result = cast(player, maxDistance);
|
||||||
|
if (result.getType() == BlockHitResult.Type.BLOCK) {
|
||||||
|
return result.getBlockPos();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vec3 getEntityPos(ServerPlayer player, double maxDistance) {
|
||||||
|
var result = cast(player, maxDistance);
|
||||||
|
if (result.getType() == BlockHitResult.Type.ENTITY) {
|
||||||
|
return result.getLocation();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vec3 getPos(ServerPlayer player, double maxDistance) {
|
||||||
|
var result = cast(player, maxDistance);
|
||||||
|
if (result.getType() != BlockHitResult.Type.MISS) {
|
||||||
|
return result.getLocation();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
package me.alexdevs.solstice.api;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ServerLocation {
|
||||||
|
protected final double x;
|
||||||
|
protected final double y;
|
||||||
|
protected final double z;
|
||||||
|
protected final float yaw;
|
||||||
|
protected final float pitch;
|
||||||
|
protected final String world;
|
||||||
|
|
||||||
|
public ServerLocation(double x, double y, double z, float yaw, float pitch, ServerLevel world) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.yaw = yaw;
|
||||||
|
this.pitch = pitch;
|
||||||
|
this.world = world.dimension().location().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerLocation(ServerPlayer player) {
|
||||||
|
this.x = player.getX();
|
||||||
|
this.y = player.getY();
|
||||||
|
this.z = player.getZ();
|
||||||
|
this.yaw = player.getYRot();
|
||||||
|
this.pitch = player.getXRot();
|
||||||
|
this.world = player.serverLevel().dimension().location().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerLocation(double x, double y, double z, float yaw, float pitch, String worldKey) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.yaw = yaw;
|
||||||
|
this.pitch = pitch;
|
||||||
|
this.world = worldKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
ServerLocation that = (ServerLocation) o;
|
||||||
|
return Double.compare(getX(), that.getX()) == 0 && Double.compare(getY(), that.getY()) == 0 && Double.compare(getZ(), that.getZ()) == 0 && Float.compare(getYaw(), that.getYaw()) == 0 && Float.compare(getPitch(), that.getPitch()) == 0 && Objects.equals(getWorld(), that.getWorld());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(getX(), getY(), getZ(), getYaw(), getPitch(), getWorld());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void teleport(ServerPlayer player, boolean setBackPosition) {
|
||||||
|
if (setBackPosition) {
|
||||||
|
var currentPosition = new ServerLocation(player);
|
||||||
|
//Solstice.modules.getModule(BackModule.class).lastPlayerPositions.put(player.getUUID(), currentPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverWorld = getWorld(player.getServer());
|
||||||
|
|
||||||
|
player.setDeltaMovement(player.getDeltaMovement().multiply(1f, 0f, 1f));
|
||||||
|
player.setOnGround(true);
|
||||||
|
|
||||||
|
player.teleportTo(serverWorld, this.getX(), this.getY(), this.getZ(), this.getYaw(), this.getPitch());
|
||||||
|
|
||||||
|
// There is a bug (presumably in Fabric's api) that causes experience level to be set to 0 when teleporting between dimensions/worlds.
|
||||||
|
// Therefore, this will update the experience client side as a temporary solution.
|
||||||
|
player.giveExperiencePoints(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void teleport(ServerPlayer player) {
|
||||||
|
teleport(player, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceKey<Level> getWorldKey() {
|
||||||
|
return ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(this.getWorld()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerLevel getWorld(MinecraftServer server) {
|
||||||
|
return server.getLevel(getWorldKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockPos getBlockPos() {
|
||||||
|
return BlockPos.containing(this.getX(), this.getY(), this.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getY() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getZ() {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getYaw() {
|
||||||
|
return yaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPitch() {
|
||||||
|
return pitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWorld() {
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDistance(ServerLocation other) {
|
||||||
|
if (!other.getWorld().equals(this.getWorld())) {
|
||||||
|
return Double.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
var thisVec = new Vec3(this.getX(), this.getY(), this.getZ());
|
||||||
|
var otherVec = new Vec3(other.getX(), other.getY(), other.getZ());
|
||||||
|
|
||||||
|
return thisVec.distanceTo(otherVec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec3 getDelta(ServerLocation other) {
|
||||||
|
return new Vec3(this.getX() - other.getX(), this.getY() - other.getY(), this.getZ() - other.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package me.alexdevs.solstice.api.color;
|
||||||
|
|
||||||
|
import net.minecraft.network.chat.TextColor;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public class Gradient {
|
||||||
|
public static TextColor lerp(final float t, final @NotNull RGBColor a, final @NotNull RGBColor b) {
|
||||||
|
final float clampedT = Math.min(1.0f, Math.max(0.0f, t));
|
||||||
|
final int ar = a.red();
|
||||||
|
final int br = b.red();
|
||||||
|
final int ag = a.green();
|
||||||
|
final int bg = b.green();
|
||||||
|
final int ab = a.blue();
|
||||||
|
final int bb = b.blue();
|
||||||
|
final var color = new RGBColor(
|
||||||
|
Math.round(ar + clampedT * (br - ar)),
|
||||||
|
Math.round(ag + clampedT * (bg - ag)),
|
||||||
|
Math.round(ab + clampedT * (bb - ab))
|
||||||
|
);
|
||||||
|
|
||||||
|
return TextColor.fromRgb(color.getInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package me.alexdevs.solstice.api.color;
|
||||||
|
|
||||||
|
import net.minecraft.network.chat.TextColor;
|
||||||
|
|
||||||
|
public class RGBColor {
|
||||||
|
public final int color;
|
||||||
|
|
||||||
|
public RGBColor(TextColor color) {
|
||||||
|
this.color = color.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RGBColor(int color) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RGBColor(int red, int green, int blue) {
|
||||||
|
this.color = (red << 16) | (green << 8) | blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RGBColor(byte red, byte green, byte blue) {
|
||||||
|
this.color = (red << 16) | (green << 8) | blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int red() {
|
||||||
|
return (color >> 16) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int green() {
|
||||||
|
return (color >> 8) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int blue() {
|
||||||
|
return color & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package me.alexdevs.solstice.api.command;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import me.alexdevs.solstice.api.command.flags.Flag;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Flags {
|
||||||
|
public static void parse(String input, Flag... flags) throws CommandSyntaxException {
|
||||||
|
var args = parseArgs(input);
|
||||||
|
parseFlags(args, Arrays.stream(flags).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> parseArgs(String input) {
|
||||||
|
var args = new ArrayList<String>();
|
||||||
|
var arg = new StringBuilder();
|
||||||
|
var inQuotes = false;
|
||||||
|
var escape = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < input.length(); i++) {
|
||||||
|
var c = input.charAt(i);
|
||||||
|
|
||||||
|
if (escape) {
|
||||||
|
arg.append(c);
|
||||||
|
escape = false;
|
||||||
|
} else if (c == '\\') {
|
||||||
|
escape = true;
|
||||||
|
} else if (c == '"') {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
} else if (c == ' ' && !inQuotes) {
|
||||||
|
if (!arg.isEmpty()) {
|
||||||
|
args.add(arg.toString());
|
||||||
|
arg.setLength(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arg.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!arg.isEmpty()) {
|
||||||
|
args.add(arg.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseFlags(List<String> args, List<Flag> flags) throws CommandSyntaxException {
|
||||||
|
var iter = args.iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
var arg = iter.next();
|
||||||
|
if (arg.startsWith("-")) {
|
||||||
|
if (arg.startsWith("--")) {
|
||||||
|
var argName = arg.substring(2);
|
||||||
|
for (var flag : flags) {
|
||||||
|
if (argName.equals(flag.getName())) {
|
||||||
|
if (flag.acceptsValue()) {
|
||||||
|
flag.accept(iter.next());
|
||||||
|
} else {
|
||||||
|
flag.accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var f = arg.substring(1);
|
||||||
|
for (var c : f.toCharArray()) {
|
||||||
|
for (var flag : flags) {
|
||||||
|
if (flag.getShortFlags().contains(c)) {
|
||||||
|
if (flag.acceptsValue()) {
|
||||||
|
flag.accept(iter.next());
|
||||||
|
} else {
|
||||||
|
flag.accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package me.alexdevs.solstice.api.command;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import com.mojang.brigadier.suggestion.Suggestions;
|
||||||
|
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.SharedSuggestionProvider;
|
||||||
|
import net.minecraft.commands.arguments.EntityArgument;
|
||||||
|
import net.minecraft.commands.arguments.GameProfileArgument;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class LocalGameProfile {
|
||||||
|
public static GameProfile getGameProfile(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||||
|
var profiles = GameProfileArgument.getGameProfiles(context, name);
|
||||||
|
if (profiles.size() > 1) {
|
||||||
|
throw EntityArgument.ERROR_NOT_SINGLE_PLAYER.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles.iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GameProfile getProfile(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||||
|
var profileName = StringArgumentType.getString(context, name);
|
||||||
|
var profile = Solstice.getUserCache().getByName(profileName);
|
||||||
|
if(profile.isEmpty())
|
||||||
|
throw GameProfileArgument.ERROR_UNKNOWN_PLAYER.create();
|
||||||
|
|
||||||
|
return profile.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggest player name. Prefer online players, then cached.
|
||||||
|
* @param context
|
||||||
|
* @param builder
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static CompletableFuture<Suggestions> suggest(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||||
|
var server = context.getSource().getServer();
|
||||||
|
var onlinePlayers = server.getPlayerList().getPlayerNamesArray();
|
||||||
|
|
||||||
|
var input = builder.getRemainingLowerCase();
|
||||||
|
for (var player : onlinePlayers) {
|
||||||
|
if (SharedSuggestionProvider.matchesSubStr(input, player.toLowerCase())) {
|
||||||
|
return SharedSuggestionProvider.suggest(onlinePlayers, builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedPlayers = Solstice.getUserCache().getAllNames();
|
||||||
|
return SharedSuggestionProvider.suggest(cachedPlayers, builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
package me.alexdevs.solstice.api.command;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.LiteralMessage;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||||
|
import com.mojang.brigadier.suggestion.Suggestions;
|
||||||
|
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
// https://github.com/NucleusPowered/Nucleus/blob/v3/nucleus-core/src/main/java/io/github/nucleuspowered/nucleus/core/scaffold/command/parameter/TimespanParameter.java
|
||||||
|
public class TimeSpan {
|
||||||
|
public static final SimpleCommandExceptionType INVALID_TIMESPAN = new SimpleCommandExceptionType(new LiteralMessage("Invalid timespan"));
|
||||||
|
private static final Pattern minorTimeString = Pattern.compile("^\\d+$");
|
||||||
|
private static final Pattern timeString = Pattern.compile("^((\\d+)w)?((\\d+)d)?((\\d+)h)?((\\d+)m)?((\\d+)s)?$");
|
||||||
|
private static final Pattern timeStringNoEnd = Pattern.compile("^((\\d+)w)?((\\d+)d)?((\\d+)h)?((\\d+)m)?((\\d+)s)?");
|
||||||
|
private static final Pattern lastDigits = Pattern.compile("\\d+$");
|
||||||
|
private static final int secondsInMinute = 60;
|
||||||
|
private static final int secondsInHour = 60 * secondsInMinute;
|
||||||
|
private static final int secondsInDay = 24 * secondsInHour;
|
||||||
|
private static final int secondsInWeek = 7 * secondsInDay;
|
||||||
|
private static final int secondsInMonth = 30 * secondsInDay;
|
||||||
|
private static final int secondsInYear = 365 * secondsInDay;
|
||||||
|
|
||||||
|
private static int amount(@Nullable final String g, final int multiplier) {
|
||||||
|
if (g != null && !g.isEmpty()) {
|
||||||
|
return multiplier * Integer.parseUnsignedInt(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toShortString(int total) {
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
if(total >= secondsInWeek) {
|
||||||
|
builder.append(total / secondsInWeek);
|
||||||
|
builder.append("w");
|
||||||
|
total %= secondsInWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total >= secondsInDay) {
|
||||||
|
builder.append(total / secondsInDay);
|
||||||
|
builder.append("d");
|
||||||
|
total %= secondsInDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total >= secondsInHour) {
|
||||||
|
builder.append(total / secondsInHour);
|
||||||
|
builder.append("h");
|
||||||
|
total %= secondsInHour;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total >= secondsInMinute) {
|
||||||
|
builder.append(total / secondsInMinute);
|
||||||
|
builder.append("m");
|
||||||
|
total %= secondsInMinute;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total > 0) {
|
||||||
|
builder.append(total);
|
||||||
|
builder.append("s");
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fill(String locale, int unit) {
|
||||||
|
return locale.replaceAll("\\$\\{n}", String.valueOf(unit));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toLongString(int total) {
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
var locale = Solstice.localeManager.getLocale(CoreModule.ID);
|
||||||
|
|
||||||
|
var prependSpace = false;
|
||||||
|
|
||||||
|
if(total >= secondsInYear) {
|
||||||
|
var value = total / secondsInYear;
|
||||||
|
if(value == 1) {
|
||||||
|
builder.append(fill(locale.raw("~unit.year"), value));
|
||||||
|
} else {
|
||||||
|
builder.append(fill(locale.raw("~unit.years"), value));
|
||||||
|
}
|
||||||
|
total %= secondsInYear;
|
||||||
|
prependSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total >= secondsInMonth) {
|
||||||
|
if(prependSpace) {
|
||||||
|
builder.append(" ");
|
||||||
|
}
|
||||||
|
var value = total / secondsInMonth;
|
||||||
|
if(value == 1) {
|
||||||
|
builder.append(fill(locale.raw("~unit.month"), value));
|
||||||
|
} else {
|
||||||
|
builder.append(fill(locale.raw("~unit.months"), value));
|
||||||
|
}
|
||||||
|
total %= secondsInMonth;
|
||||||
|
prependSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total >= secondsInWeek) {
|
||||||
|
if(prependSpace) {
|
||||||
|
builder.append(" ");
|
||||||
|
}
|
||||||
|
var value = total / secondsInWeek;
|
||||||
|
if(value == 1) {
|
||||||
|
builder.append(fill(locale.raw("~unit.week"), value));
|
||||||
|
} else {
|
||||||
|
builder.append(fill(locale.raw("~unit.weeks"), value));
|
||||||
|
}
|
||||||
|
total %= secondsInWeek;
|
||||||
|
prependSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total >= secondsInDay) {
|
||||||
|
if(prependSpace) {
|
||||||
|
builder.append(" ");
|
||||||
|
}
|
||||||
|
var value = total / secondsInDay;
|
||||||
|
if(value == 1) {
|
||||||
|
builder.append(fill(locale.raw("~unit.day"), value));
|
||||||
|
} else {
|
||||||
|
builder.append(fill(locale.raw("~unit.days"), value));
|
||||||
|
}
|
||||||
|
total %= secondsInDay;
|
||||||
|
prependSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total >= secondsInHour) {
|
||||||
|
if(prependSpace) {
|
||||||
|
builder.append(" ");
|
||||||
|
}
|
||||||
|
var value = total / secondsInHour;
|
||||||
|
if(value == 1) {
|
||||||
|
builder.append(fill(locale.raw("~unit.hour"), value));
|
||||||
|
} else {
|
||||||
|
builder.append(fill(locale.raw("~unit.hours"), value));
|
||||||
|
}
|
||||||
|
total %= secondsInHour;
|
||||||
|
prependSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total >= secondsInMinute) {
|
||||||
|
if(prependSpace) {
|
||||||
|
builder.append(" ");
|
||||||
|
}
|
||||||
|
var value = total / secondsInMinute;
|
||||||
|
if(value == 1) {
|
||||||
|
builder.append(fill(locale.raw("~unit.minute"), value));
|
||||||
|
} else {
|
||||||
|
builder.append(fill(locale.raw("~unit.minutes"), value));
|
||||||
|
}
|
||||||
|
total %= secondsInMinute;
|
||||||
|
prependSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(total > 0) {
|
||||||
|
if(prependSpace) {
|
||||||
|
builder.append(" ");
|
||||||
|
}
|
||||||
|
if(total == 1) {
|
||||||
|
builder.append(fill(locale.raw("~unit.second"), total));
|
||||||
|
} else {
|
||||||
|
builder.append(fill(locale.raw("~unit.seconds"), total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<? extends Integer> parse(String s) {
|
||||||
|
// First, if just digits, return the number in seconds.
|
||||||
|
if (minorTimeString.matcher(s).matches()) {
|
||||||
|
return Optional.of(Integer.parseUnsignedInt(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Matcher m = timeString.matcher(s);
|
||||||
|
if (m.matches()) {
|
||||||
|
int time = amount(m.group(2), secondsInWeek);
|
||||||
|
time += amount(m.group(4), secondsInDay);
|
||||||
|
time += amount(m.group(6), secondsInHour);
|
||||||
|
time += amount(m.group(8), secondsInMinute);
|
||||||
|
time += amount(m.group(10), 1);
|
||||||
|
|
||||||
|
if (time > 0) {
|
||||||
|
return Optional.of(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getTimeSpan(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||||
|
var argument = context.getArgument(name, String.class);
|
||||||
|
var timespan = parse(argument);
|
||||||
|
if (timespan.isPresent()) {
|
||||||
|
return timespan.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw INVALID_TIMESPAN.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Suggestions> suggest(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||||
|
var original = builder.getRemainingLowerCase();
|
||||||
|
if (original.isEmpty()) {
|
||||||
|
return Suggestions.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeString.matcher(original).matches()) {
|
||||||
|
builder.suggest(original);
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
|
||||||
|
var units = List.of(
|
||||||
|
new Unit("w", "Week"),
|
||||||
|
new Unit("d", "Day"),
|
||||||
|
new Unit("h", "Hour"),
|
||||||
|
new Unit("m", "Minute"),
|
||||||
|
new Unit("s", "Second")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (minorTimeString.matcher(original).matches()) {
|
||||||
|
for (var unit : units) {
|
||||||
|
builder.suggest(original + unit.unit, new LiteralMessage(unit.tooltip));
|
||||||
|
}
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (timeStringNoEnd.matcher(original).find() && lastDigits.matcher(original).find()) {
|
||||||
|
var max = 0;
|
||||||
|
for (var i = 0; i < units.size(); i++) {
|
||||||
|
if (original.contains(units.get(i).unit))
|
||||||
|
max = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = max; i < units.size(); i++) {
|
||||||
|
var unit = units.get(i);
|
||||||
|
if (!original.contains(unit.unit)) {
|
||||||
|
builder.suggest(original + unit.unit, new LiteralMessage(unit.tooltip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StringArgumentType timeSpan() {
|
||||||
|
return StringArgumentType.word();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Unit(String unit, String tooltip) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package me.alexdevs.solstice.api.command.flags;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public abstract class ArgumentFlag<T> extends Flag {
|
||||||
|
public ArgumentFlag(String name, List<Character> shortNames) {
|
||||||
|
super(name, shortNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract T getValue();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract void accept(String input) throws CommandSyntaxException;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean acceptsValue() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract ArgumentFlag<T> clone();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package me.alexdevs.solstice.api.command.flags;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.StringReader;
|
||||||
|
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DoubleFlag extends ArgumentFlag<Double> {
|
||||||
|
protected double value;
|
||||||
|
protected final DoubleArgumentType type;
|
||||||
|
|
||||||
|
public DoubleFlag(String name, List<Character> shortFlags) {
|
||||||
|
super(name, shortFlags);
|
||||||
|
type = DoubleArgumentType.doubleArg();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Double getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(String input) throws CommandSyntaxException {
|
||||||
|
this.isUsed = true;
|
||||||
|
value = type.parse(new StringReader(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DoubleFlag clone() {
|
||||||
|
return new DoubleFlag(name, shortFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package me.alexdevs.solstice.api.command.flags;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.LiteralMessage;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Flag implements Comparable<Flag> {
|
||||||
|
public static final SimpleCommandExceptionType UNEXPECTED_VALUE = new SimpleCommandExceptionType(new LiteralMessage("Unexpected value"));
|
||||||
|
|
||||||
|
protected final String name;
|
||||||
|
protected final List<Character> shortFlags = new ArrayList<>();
|
||||||
|
protected boolean isUsed = false;
|
||||||
|
|
||||||
|
public Flag(String name, List<Character> shortNames) {
|
||||||
|
this.name = name;
|
||||||
|
this.shortFlags.addAll(shortNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Character> getShortFlags() {
|
||||||
|
return Collections.unmodifiableList(shortFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean acceptsValue() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUsed() {
|
||||||
|
return isUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flag clone() {
|
||||||
|
return new Flag(name, shortFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void accept(String value) throws CommandSyntaxException {
|
||||||
|
throw UNEXPECTED_VALUE.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void accept() {
|
||||||
|
isUsed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Flag of(String name) {
|
||||||
|
return new Flag(name, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NotNull Flag o) {
|
||||||
|
return this.name.compareTo(o.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package me.alexdevs.solstice.api.command.flags;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.StringReader;
|
||||||
|
import com.mojang.brigadier.arguments.FloatArgumentType;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FloatFlag extends ArgumentFlag<Float> {
|
||||||
|
protected Float value;
|
||||||
|
protected final FloatArgumentType type;
|
||||||
|
|
||||||
|
public FloatFlag(String name, List<Character> shortFlags) {
|
||||||
|
super(name, shortFlags);
|
||||||
|
type = FloatArgumentType.floatArg();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Float getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(String input) throws CommandSyntaxException {
|
||||||
|
this.isUsed = true;
|
||||||
|
value = type.parse(new StringReader(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FloatFlag clone() {
|
||||||
|
return new FloatFlag(name, shortFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package me.alexdevs.solstice.api.command.flags;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.StringReader;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StringFlag extends ArgumentFlag<String> {
|
||||||
|
protected String value;
|
||||||
|
protected final StringArgumentType type;
|
||||||
|
public StringFlag(String name, List<Character> shortFlags) {
|
||||||
|
super(name, shortFlags);
|
||||||
|
type = StringArgumentType.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(String input) throws CommandSyntaxException {
|
||||||
|
this.isUsed = true;
|
||||||
|
value = type.parse(new StringReader(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String get(String name, List<Flag> flags) {
|
||||||
|
for(var flag : flags) {
|
||||||
|
if(flag.getName().equals(name) && flag instanceof StringFlag stringFlag) {
|
||||||
|
return stringFlag.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StringFlag clone() {
|
||||||
|
return new StringFlag(name, shortFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package me.alexdevs.solstice.api.data;
|
||||||
|
|
||||||
|
import io.leangen.geantyref.TypeToken;
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.api.data.serializers.DateSerializer;
|
||||||
|
import org.spongepowered.configurate.BasicConfigurationNode;
|
||||||
|
import org.spongepowered.configurate.ConfigurateException;
|
||||||
|
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
|
||||||
|
import org.spongepowered.configurate.serialize.TypeSerializerCollection;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class GsonDataManager {
|
||||||
|
protected final Map<String, Class<?>> classMap = new HashMap<>();
|
||||||
|
protected final Map<Class<?>, Object> data = new HashMap<>();
|
||||||
|
protected final Map<Class<?>, Supplier<?>> providers = new HashMap<>();
|
||||||
|
protected Path filePath;
|
||||||
|
protected GsonConfigurationLoader loader;
|
||||||
|
protected BasicConfigurationNode dataNode;
|
||||||
|
|
||||||
|
protected static GsonConfigurationLoader getLoader(Path path) {
|
||||||
|
return GsonConfigurationLoader
|
||||||
|
.builder()
|
||||||
|
.path(path)
|
||||||
|
.defaultOptions(opts -> opts
|
||||||
|
.shouldCopyDefaults(true)
|
||||||
|
.serializers(TypeSerializerCollection.defaults()
|
||||||
|
.childBuilder()
|
||||||
|
.registerExact(DateSerializer.TYPE)
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getDataPath() {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataPath(Path filePath) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
loader = getLoader(getDataPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getData(Class<T> clazz) {
|
||||||
|
if (this.data.containsKey(clazz)) {
|
||||||
|
return (T) this.data.get(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.providers.containsKey(clazz)) {
|
||||||
|
final T result = (T) this.providers.get(clazz).get();
|
||||||
|
this.data.put(clazz, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException(clazz.getSimpleName() + " does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
for (var entry : classMap.entrySet()) {
|
||||||
|
try {
|
||||||
|
dataNode.node(entry.getKey()).set(data.get(entry.getValue()));
|
||||||
|
} catch (ConfigurateException e) {
|
||||||
|
Solstice.LOGGER.error("Could not save file {} data for {}. Skipping", this.filePath, entry.getKey(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loader.save(dataNode);
|
||||||
|
} catch (ConfigurateException e) {
|
||||||
|
Solstice.LOGGER.error("Could not save file {} data", this.filePath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void registerData(String id, Class<T> clazz, Supplier<T> creator) {
|
||||||
|
classMap.put(id, clazz);
|
||||||
|
providers.put(clazz, creator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadData(boolean force) throws ConfigurateException {
|
||||||
|
if (dataNode == null || force) {
|
||||||
|
dataNode = loader.load();
|
||||||
|
}
|
||||||
|
data.clear();
|
||||||
|
for (var entry : classMap.entrySet()) {
|
||||||
|
try {
|
||||||
|
data.put(entry.getValue(), get(dataNode.node(entry.getKey()), entry.getValue()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Solstice.LOGGER.error("Could not load file {} data for {}. Using default values.", this.filePath, entry.getKey(), e);
|
||||||
|
this.data.put(entry.getValue(), dataNode.node(entry.getKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T get(final BasicConfigurationNode node, final Class<T> clazz) throws ConfigurateException {
|
||||||
|
return node.get(TypeToken.get(clazz), (Supplier<T>) () -> (T) this.providers.get(clazz).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> void set(final BasicConfigurationNode node, final Class<T> clazz) throws ConfigurateException {
|
||||||
|
node.set(TypeToken.get(clazz), (T) this.providers.get(clazz).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prepareData() throws ConfigurateException {
|
||||||
|
var node = loader.load();
|
||||||
|
var defaults = loader.createNode();
|
||||||
|
|
||||||
|
for (var map : classMap.entrySet()) {
|
||||||
|
set(defaults.node(map.getKey()), map.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
node.mergeFrom(defaults);
|
||||||
|
loader.save(node);
|
||||||
|
this.dataNode = node;
|
||||||
|
loadData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package me.alexdevs.solstice.api.data;
|
||||||
|
|
||||||
|
import io.leangen.geantyref.TypeToken;
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.api.data.serializers.DateSerializer;
|
||||||
|
import org.spongepowered.configurate.CommentedConfigurationNode;
|
||||||
|
import org.spongepowered.configurate.ConfigurateException;
|
||||||
|
import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
|
||||||
|
import org.spongepowered.configurate.serialize.TypeSerializerCollection;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class HoconDataManager {
|
||||||
|
protected final Map<String, Class<?>> classMap = new HashMap<>();
|
||||||
|
protected final Map<Class<?>, Object> data = new HashMap<>();
|
||||||
|
protected final Map<Class<?>, Supplier<?>> providers = new HashMap<>();
|
||||||
|
protected Path filePath;
|
||||||
|
protected HoconConfigurationLoader loader;
|
||||||
|
protected CommentedConfigurationNode dataNode;
|
||||||
|
|
||||||
|
public HoconDataManager() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public HoconDataManager(final Path filePath) {
|
||||||
|
setDataPath(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static HoconConfigurationLoader getLoader(Path path) {
|
||||||
|
return HoconConfigurationLoader
|
||||||
|
.builder()
|
||||||
|
.path(path)
|
||||||
|
.defaultOptions(opts -> opts
|
||||||
|
.shouldCopyDefaults(true)
|
||||||
|
.serializers(TypeSerializerCollection.defaults()
|
||||||
|
.childBuilder()
|
||||||
|
.registerExact(DateSerializer.TYPE)
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getDataPath() {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataPath(Path filePath) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
loader = getLoader(getDataPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getData(Class<T> clazz) {
|
||||||
|
if (this.data.containsKey(clazz)) {
|
||||||
|
return (T) this.data.get(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.providers.containsKey(clazz)) {
|
||||||
|
final T result = (T) this.providers.get(clazz).get();
|
||||||
|
this.data.put(clazz, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException(clazz.getSimpleName() + " does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
for (var entry : classMap.entrySet()) {
|
||||||
|
try {
|
||||||
|
dataNode.node(entry.getKey()).set(data.get(entry.getValue()));
|
||||||
|
} catch (ConfigurateException e) {
|
||||||
|
Solstice.LOGGER.error("Could not save server data for {}. Skipping", entry.getKey(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loader.save(dataNode);
|
||||||
|
} catch (ConfigurateException e) {
|
||||||
|
Solstice.LOGGER.error("Could not save server data to file!", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void registerData(String id, Class<T> clazz, Supplier<T> creator) {
|
||||||
|
classMap.put(id, clazz);
|
||||||
|
providers.put(clazz, creator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadData(boolean force) throws ConfigurateException {
|
||||||
|
if (dataNode == null || force) {
|
||||||
|
dataNode = loader.load();
|
||||||
|
}
|
||||||
|
data.clear();
|
||||||
|
for (var entry : classMap.entrySet()) {
|
||||||
|
try {
|
||||||
|
data.put(entry.getValue(), get(dataNode.node(entry.getKey()), entry.getValue()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Solstice.LOGGER.error("Could not load server data for {}. Using default values.", entry.getKey(), e);
|
||||||
|
this.data.put(entry.getValue(), dataNode.node(entry.getKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <T> T get(final CommentedConfigurationNode node, final Class<T> clazz) throws ConfigurateException {
|
||||||
|
return node.get(TypeToken.get(clazz), (Supplier<T>) () -> (T) this.providers.get(clazz).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <T> void set(final CommentedConfigurationNode node, final Class<T> clazz) throws ConfigurateException {
|
||||||
|
node.set(TypeToken.get(clazz), (T) this.providers.get(clazz).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prepareData() throws ConfigurateException {
|
||||||
|
var node = loader.load();
|
||||||
|
var defaults = loader.createNode();
|
||||||
|
|
||||||
|
for (var map : classMap.entrySet()) {
|
||||||
|
set(defaults.node(map.getKey()), map.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
node.mergeFrom(defaults);
|
||||||
|
loader.save(node);
|
||||||
|
this.dataNode = node;
|
||||||
|
loadData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package me.alexdevs.solstice.api.data.serializers;
|
||||||
|
|
||||||
|
import org.spongepowered.configurate.serialize.ScalarSerializer;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public final class DateSerializer extends ScalarSerializer<Date> {
|
||||||
|
public static final DateSerializer TYPE = new DateSerializer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO 8601 Date format
|
||||||
|
*/
|
||||||
|
public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX";
|
||||||
|
public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat(DATE_FORMAT);
|
||||||
|
|
||||||
|
DateSerializer() {
|
||||||
|
super(Date.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date deserialize(final Type type, final Object obj) {
|
||||||
|
try {
|
||||||
|
return DATE_FORMATTER.parse(obj.toString());
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object serialize(final Date item, final Predicate<Class<?>> typeSupported) {
|
||||||
|
return DATE_FORMATTER.format(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package me.alexdevs.solstice.api.events;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
|
||||||
|
public class CommandEvents {
|
||||||
|
public static final Event<AllowCommand> ALLOW_COMMAND = EventFactory.createArrayBacked(AllowCommand.class, callbacks ->
|
||||||
|
(source, command) -> {
|
||||||
|
for (var callback : callbacks) {
|
||||||
|
if(!callback.allowCommand(source, command))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<Command> COMMAND = EventFactory.createArrayBacked(Command.class, callbacks ->
|
||||||
|
(player, command) -> {
|
||||||
|
for (var callback : callbacks) {
|
||||||
|
callback.onCommand(player, command);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface AllowCommand {
|
||||||
|
boolean allowCommand(CommandSourceStack source, String command);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Command {
|
||||||
|
void onCommand(CommandSourceStack source, String command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package me.alexdevs.solstice.api.events;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.modules.afk.AfkModule;
|
||||||
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
public final class PlayerActivityEvents {
|
||||||
|
|
||||||
|
public static final Event<Afk> AFK = EventFactory.createArrayBacked(Afk.class, callbacks -> (handler) -> {
|
||||||
|
for (Afk callback : callbacks) {
|
||||||
|
callback.onAfk(handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<AfkReturn> AFK_RETURN = EventFactory.createArrayBacked(AfkReturn.class, callbacks -> (handler, reason) -> {
|
||||||
|
for (AfkReturn callback : callbacks) {
|
||||||
|
callback.onAfkReturn(handler, reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Afk {
|
||||||
|
void onAfk(ServerPlayer player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface AfkReturn {
|
||||||
|
void onAfkReturn(ServerPlayer player, AfkModule.AfkTriggerReason reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package me.alexdevs.solstice.api.events;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
|
||||||
|
public class PlayerConnectionEvents {
|
||||||
|
public static final Event<WhitelistBypass> WHITELIST_BYPASS = EventFactory.createArrayBacked(WhitelistBypass.class, callbacks ->
|
||||||
|
(profile) -> {
|
||||||
|
for (var callback : callbacks) {
|
||||||
|
if (callback.bypassWhitelist(profile))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final Event<FullServerBypass> FULL_SERVER_BYPASS = EventFactory.createArrayBacked(FullServerBypass.class, callbacks ->
|
||||||
|
(profile) -> {
|
||||||
|
for (var callback : callbacks) {
|
||||||
|
if (callback.bypassFullServer(profile))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface WhitelistBypass {
|
||||||
|
boolean bypassWhitelist(GameProfile profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface FullServerBypass {
|
||||||
|
boolean bypassFullServer(GameProfile profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package me.alexdevs.solstice.api.events;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.api.ServerLocation;
|
||||||
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
public interface PlayerTeleportCallback {
|
||||||
|
Event<PlayerTeleportCallback> EVENT = EventFactory.createArrayBacked(PlayerTeleportCallback.class,
|
||||||
|
(listeners) -> (player, origin, destination) -> {
|
||||||
|
for (PlayerTeleportCallback listener : listeners) {
|
||||||
|
listener.teleport(player, origin, destination);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void teleport(ServerPlayer player, ServerLocation origin, ServerLocation destination);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package me.alexdevs.solstice.api.events;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.modules.timeBar.TimeBar;
|
||||||
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
|
||||||
|
public class RestartEvents {
|
||||||
|
public static final Event<Schedule> SCHEDULED = EventFactory.createArrayBacked(Schedule.class, callbacks ->
|
||||||
|
(timeBar, type) -> {
|
||||||
|
for (Schedule callback : callbacks) {
|
||||||
|
callback.onSchedule(timeBar, type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<Cancel> CANCELED = EventFactory.createArrayBacked(Cancel.class, callbacks ->
|
||||||
|
(timeBar) -> {
|
||||||
|
for (Cancel callback : callbacks) {
|
||||||
|
callback.onCancel(timeBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Schedule {
|
||||||
|
void onSchedule(TimeBar timeBar, RestartType type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Cancel {
|
||||||
|
void onCancel(TimeBar timeBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RestartType {
|
||||||
|
AUTOMATIC,
|
||||||
|
MANUAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package me.alexdevs.solstice.api.events;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
public final class SolsticeEvents {
|
||||||
|
public static final Event<Ready> READY = EventFactory.createArrayBacked(Ready.class, callbacks ->
|
||||||
|
(instance, server) -> {
|
||||||
|
for (Ready callback : callbacks) {
|
||||||
|
callback.onReady(instance, server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<Reload> RELOAD = EventFactory.createArrayBacked(Reload.class, callbacks ->
|
||||||
|
(instance) -> {
|
||||||
|
for (Reload callback : callbacks) {
|
||||||
|
callback.onReload(instance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<Welcome> WELCOME = EventFactory.createArrayBacked(Welcome.class, callbacks ->
|
||||||
|
(player, server) -> {
|
||||||
|
for (Welcome callback : callbacks) {
|
||||||
|
callback.onWelcome(player, server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<UsernameChange> USERNAME_CHANGE = EventFactory.createArrayBacked(UsernameChange.class, callbacks ->
|
||||||
|
(player, previousUsername) -> {
|
||||||
|
for (UsernameChange callback : callbacks) {
|
||||||
|
callback.onUsernameChange(player, previousUsername);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Superseded by {@link CommandEvents}
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static final Event<PlayerCommand> PLAYER_COMMAND = EventFactory.createArrayBacked(PlayerCommand.class, callbacks ->
|
||||||
|
(player, command) -> {
|
||||||
|
for (PlayerCommand callback : callbacks) {
|
||||||
|
callback.onPlayerCommand(player, command);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Ready {
|
||||||
|
void onReady(Solstice instance, MinecraftServer server);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Reload {
|
||||||
|
void onReload(Solstice instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Welcome {
|
||||||
|
void onWelcome(ServerPlayer player, MinecraftServer server);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface UsernameChange {
|
||||||
|
void onUsernameChange(ServerPlayer player, String previousUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface PlayerCommand {
|
||||||
|
void onPlayerCommand(ServerPlayer player, String command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package me.alexdevs.solstice.api.events;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.modules.timeBar.TimeBar;
|
||||||
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
|
||||||
|
public class TimeBarEvents {
|
||||||
|
public static final Event<Start> START = EventFactory.createArrayBacked(Start.class, callbacks ->
|
||||||
|
(timeBar, server) -> {
|
||||||
|
for (Start callback : callbacks) {
|
||||||
|
callback.onStart(timeBar, server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<End> END = EventFactory.createArrayBacked(End.class, callbacks ->
|
||||||
|
(timeBar, server) -> {
|
||||||
|
for (End callback : callbacks) {
|
||||||
|
callback.onEnd(timeBar, server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<Cancel> CANCEL = EventFactory.createArrayBacked(Cancel.class, callbacks ->
|
||||||
|
(timeBar, server) -> {
|
||||||
|
for (Cancel callback : callbacks) {
|
||||||
|
callback.onCancel(timeBar, server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<Progress> PROGRESS = EventFactory.createArrayBacked(Progress.class, callbacks ->
|
||||||
|
(timeBar, server) -> {
|
||||||
|
for (Progress callback : callbacks) {
|
||||||
|
callback.onProgress(timeBar, server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Start {
|
||||||
|
void onStart(TimeBar timeBar, MinecraftServer server);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface End {
|
||||||
|
void onEnd(TimeBar timeBar, MinecraftServer server);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Cancel {
|
||||||
|
void onCancel(TimeBar timeBar, MinecraftServer server);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Progress {
|
||||||
|
void onProgress(TimeBar timeBar, MinecraftServer server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package me.alexdevs.solstice.api.events;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
|
||||||
|
public interface WorldSaveCallback {
|
||||||
|
Event<WorldSaveCallback> EVENT = EventFactory.createArrayBacked(WorldSaveCallback.class, (callbacks) ->
|
||||||
|
(server, suppressLogs, flush, force) -> {
|
||||||
|
for (WorldSaveCallback callback : callbacks) {
|
||||||
|
callback.onSave(server, suppressLogs, flush, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
void onSave(MinecraftServer server, boolean suppressLogs, boolean flush, boolean force);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package me.alexdevs.solstice.api.module;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class Debug {
|
||||||
|
public static final HashSet<CommandDebug> commandDebugList = new HashSet<>();
|
||||||
|
|
||||||
|
public record CommandDebug(String module, String command, List<String> commands, String permission) {
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof CommandDebug that)) return false;
|
||||||
|
return Objects.equals(module, that.module) && Objects.equals(command, that.command) && Objects.equals(permission, that.permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(module, command, permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
package me.alexdevs.solstice.api.module;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||||
|
import me.lucko.fabric.api.permissions.v0.Permissions;
|
||||||
|
import net.minecraft.commands.CommandBuildContext;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public abstract class ModCommand<T extends ModuleBase> {
|
||||||
|
protected final T module;
|
||||||
|
protected CommandDispatcher<CommandSourceStack> dispatcher;
|
||||||
|
protected CommandBuildContext commandRegistry;
|
||||||
|
protected Commands.CommandSelection environment;
|
||||||
|
|
||||||
|
public ModCommand(T module) {
|
||||||
|
this.commandRegistry = null;
|
||||||
|
this.environment = null;
|
||||||
|
this.dispatcher = null;
|
||||||
|
|
||||||
|
this.module = module;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandRegistry, Commands.CommandSelection environment) {
|
||||||
|
this.dispatcher = dispatcher;
|
||||||
|
this.commandRegistry = commandRegistry;
|
||||||
|
this.environment = environment;
|
||||||
|
|
||||||
|
var aliases = new ArrayList<>(getNames());
|
||||||
|
var name = aliases.remove(0);
|
||||||
|
var node = registerCommand(command(name));
|
||||||
|
|
||||||
|
for (var alias : aliases) {
|
||||||
|
dispatcher.register(Commands.literal(alias)
|
||||||
|
.requires(node.getRequirement())
|
||||||
|
.executes(node.getCommand())
|
||||||
|
.redirect(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiteralCommandNode<CommandSourceStack> registerCommand(LiteralArgumentBuilder<CommandSourceStack> command) {
|
||||||
|
return dispatcher.register(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return getNames().stream().findFirst().orElseGet(() -> this.getClass().getSimpleName().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPermissionNode() {
|
||||||
|
var node = module.getPermissionNode("base");
|
||||||
|
Debug.commandDebugList.add(new Debug.CommandDebug(module.id, getName(), getNames(), node));
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPermissionNode(String subNode) {
|
||||||
|
var node = module.getPermissionNode(subNode);
|
||||||
|
Debug.commandDebugList.add(new Debug.CommandDebug(module.id, getName(), getNames(), node));
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predicate<CommandSourceStack> require() {
|
||||||
|
return Permissions.require(getPermissionNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predicate<CommandSourceStack> require(int defaultRequiredLevel) {
|
||||||
|
return Permissions.require(getPermissionNode(), defaultRequiredLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predicate<CommandSourceStack> require(boolean defaultValue) {
|
||||||
|
return Permissions.require(getPermissionNode(), defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predicate<CommandSourceStack> require(String subNode) {
|
||||||
|
return Permissions.require(getPermissionNode(subNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predicate<CommandSourceStack> require(String subNode, int defaultRequiredLevel) {
|
||||||
|
return Permissions.require(getPermissionNode(subNode), defaultRequiredLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predicate<CommandSourceStack> require(String subNode, boolean defaultValue) {
|
||||||
|
return Permissions.require(getPermissionNode(subNode), defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the name and aliases of the command. First value is the name, next values are aliases.
|
||||||
|
*
|
||||||
|
* @return List of names
|
||||||
|
*/
|
||||||
|
public abstract List<String> getNames();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the command node, this method gets called for every name.
|
||||||
|
*
|
||||||
|
* @param name Command name
|
||||||
|
* @return Command node
|
||||||
|
*/
|
||||||
|
public abstract LiteralArgumentBuilder<CommandSourceStack> command(String name);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package me.alexdevs.solstice.api.module;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.core.ToggleableConfig;
|
||||||
|
import me.alexdevs.solstice.locale.Locale;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public abstract class ModuleBase implements Comparable<ModuleBase> {
|
||||||
|
protected final String id;
|
||||||
|
protected final List<ModCommand<?>> commands = new ArrayList<>();
|
||||||
|
|
||||||
|
public ModuleBase(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when Solstice is ready to initialize modules.
|
||||||
|
* <p>
|
||||||
|
* If the module is a {@linkplain Toggleable}, it will check if it is enabled before calling it.
|
||||||
|
*/
|
||||||
|
public abstract void init();
|
||||||
|
|
||||||
|
public Collection<? extends ModCommand<?>> getCommands() {
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPermissionNode() {
|
||||||
|
return Solstice.MOD_ID + "." + id.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPermissionNode(String sub) {
|
||||||
|
return getPermissionNode() + "." + sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Locale locale() {
|
||||||
|
return Solstice.localeManager.getLocale(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Module [" + id + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof ModuleBase that)) return false;
|
||||||
|
return Objects.equals(id, that.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(ModuleBase o) {
|
||||||
|
return id.compareTo(o.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class Toggleable extends ModuleBase {
|
||||||
|
public Toggleable(String id) {
|
||||||
|
super(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return ToggleableConfig.get().isEnabled(this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package me.alexdevs.solstice.api.module;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modules provider for Solstice.
|
||||||
|
*
|
||||||
|
* <p>In {@code fabric.mod.json}, the entrypoint is defined with {@code solstice} key.</p>
|
||||||
|
*
|
||||||
|
* Provide a set of {@code ModuleBase} modules to register.
|
||||||
|
*
|
||||||
|
* @see ModuleBase
|
||||||
|
*/
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ModuleEntrypoint {
|
||||||
|
HashSet<ModuleBase> register();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package me.alexdevs.solstice.api.module;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
|
||||||
|
public class Utils {
|
||||||
|
public static void removeCommands(CommandDispatcher<?> dispatcher, String... commandNames) {
|
||||||
|
for (String commandName : commandNames) {
|
||||||
|
var command = dispatcher.getRoot().getChild(commandName);
|
||||||
|
if (command != null) {
|
||||||
|
dispatcher.getRoot().getChildren().remove(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package me.alexdevs.solstice.api.text;
|
||||||
|
|
||||||
|
import eu.pb4.placeholders.api.TextParserUtils;
|
||||||
|
import eu.pb4.placeholders.api.parsers.NodeParser;
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.api.text.parser.MarkdownParser;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import me.alexdevs.solstice.modules.styling.StylingModule;
|
||||||
|
import me.alexdevs.solstice.modules.styling.data.StylingConfig;
|
||||||
|
import me.lucko.fabric.api.permissions.v0.Permissions;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.PlayerChatMessage;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Components {
|
||||||
|
public static Component button(Component label, Component hoverText, String command, boolean suggest) {
|
||||||
|
var locale = Solstice.localeManager.getLocale(CoreModule.ID);
|
||||||
|
var format = suggest ? locale.raw("~buttonSuggest") : locale.raw("~button");
|
||||||
|
var placeholders = Map.of(
|
||||||
|
"label", label,
|
||||||
|
"hoverText", hoverText,
|
||||||
|
"command", Component.nullToEmpty(command)
|
||||||
|
);
|
||||||
|
|
||||||
|
var text = TextParserUtils.formatText(format);
|
||||||
|
return Format.parse(text, placeholders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component button(String label, String hoverText, String command) {
|
||||||
|
|
||||||
|
return button(
|
||||||
|
Format.parse(label),
|
||||||
|
Format.parse(hoverText),
|
||||||
|
command,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component buttonSuggest(String label, String hoverText, String command) {
|
||||||
|
|
||||||
|
return button(
|
||||||
|
Format.parse(label),
|
||||||
|
Format.parse(hoverText),
|
||||||
|
command,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component chat(PlayerChatMessage message, ServerPlayer player) {
|
||||||
|
var allowAdvancedChatFormat = Permissions.check(player, StylingModule.ADVANCED_CHAT_FORMATTING_PERMISSION);
|
||||||
|
|
||||||
|
return chat(message.signedContent(), allowAdvancedChatFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component chat(String message, ServerPlayer player) {
|
||||||
|
var allowAdvancedChatFormat = Permissions.check(player, StylingModule.ADVANCED_CHAT_FORMATTING_PERMISSION);
|
||||||
|
|
||||||
|
return chat(message, allowAdvancedChatFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component chat(String message, boolean allowAdvancedChatFormat) {
|
||||||
|
var config = Solstice.configManager.getData(StylingConfig.class);
|
||||||
|
var enableMarkdown = config.enableMarkdown;
|
||||||
|
|
||||||
|
for (var repl : config.replacements.entrySet()) {
|
||||||
|
message = message.replace(repl.getKey(), repl.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowAdvancedChatFormat && !enableMarkdown) {
|
||||||
|
return Component.nullToEmpty(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeParser parser;
|
||||||
|
if (allowAdvancedChatFormat) {
|
||||||
|
parser = NodeParser.merge(Format.PARSER, MarkdownParser.defaultParser);
|
||||||
|
} else {
|
||||||
|
parser = MarkdownParser.defaultParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parser.parseNode(message).toText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component chat(String message, CommandSourceStack source) {
|
||||||
|
if (source.isPlayer())
|
||||||
|
return chat(message, source.getPlayer());
|
||||||
|
return chat(message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package me.alexdevs.solstice.api.text;
|
||||||
|
|
||||||
|
import eu.pb4.placeholders.api.PlaceholderContext;
|
||||||
|
import eu.pb4.placeholders.api.Placeholders;
|
||||||
|
import eu.pb4.placeholders.api.node.TextNode;
|
||||||
|
import eu.pb4.placeholders.api.parsers.NodeParser;
|
||||||
|
import eu.pb4.placeholders.api.parsers.PatternPlaceholderParser;
|
||||||
|
import eu.pb4.placeholders.api.parsers.TextParserV1;
|
||||||
|
import me.alexdevs.solstice.api.text.tag.PhaseGradientTag;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class Format {
|
||||||
|
public static final Pattern PLACEHOLDER_PATTERN = PatternPlaceholderParser.PREDEFINED_PLACEHOLDER_PATTERN;
|
||||||
|
public static final NodeParser PARSER;
|
||||||
|
|
||||||
|
static {
|
||||||
|
var parser = TextParserV1.createDefault();
|
||||||
|
parser.register(PhaseGradientTag.createTag());
|
||||||
|
PARSER = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component parse(String text) {
|
||||||
|
return PARSER.parseNode(text).toText(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component parse(TextNode textNode, PlaceholderContext context, Map<String, Component> placeholders) {
|
||||||
|
var predefinedNode = Placeholders.parseNodes(textNode, PLACEHOLDER_PATTERN, placeholders);
|
||||||
|
return Placeholders.parseText(predefinedNode, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component parse(Component text, PlaceholderContext context, Map<String, Component> placeholders) {
|
||||||
|
return parse(TextNode.convert(text), context, placeholders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component parse(String text, PlaceholderContext context, Map<String, Component> placeholders) {
|
||||||
|
return parse(parse(text), context, placeholders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component parse(String text, PlaceholderContext context) {
|
||||||
|
return parse(parse(text), context, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component parse(String text, Map<String, Component> placeholders) {
|
||||||
|
return Placeholders.parseText(parse(text), PLACEHOLDER_PATTERN, placeholders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component parse(Component text, Map<String, Component> placeholders) {
|
||||||
|
return Placeholders.parseText(TextNode.convert(text), PLACEHOLDER_PATTERN, placeholders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package me.alexdevs.solstice.api.text;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class RawPlaceholder {
|
||||||
|
public static final Pattern PATTERN = Format.PLACEHOLDER_PATTERN;
|
||||||
|
|
||||||
|
public static String parse(String input, Map<String, String> placeholders) {
|
||||||
|
var matcher = PATTERN.matcher(input);
|
||||||
|
while (matcher.find()) {
|
||||||
|
var chunk = matcher.group();
|
||||||
|
var key = matcher.group("id");
|
||||||
|
input = input.replace(chunk, placeholders.getOrDefault(key, ""));
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package me.alexdevs.solstice.api.text.parser;
|
||||||
|
|
||||||
|
import eu.pb4.placeholders.api.node.DirectTextNode;
|
||||||
|
import eu.pb4.placeholders.api.node.LiteralNode;
|
||||||
|
import eu.pb4.placeholders.api.node.TextNode;
|
||||||
|
import eu.pb4.placeholders.api.parsers.NodeParser;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.ClickEvent;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.HoverEvent;
|
||||||
|
import net.minecraft.network.chat.Style;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class CodeParser implements NodeParser {
|
||||||
|
public static final Pattern CODE_REGEX = Pattern.compile("(?<!\\\\)`(.*?)(?<!\\\\)`");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TextNode[] parseNodes(TextNode node) {
|
||||||
|
if (node instanceof LiteralNode literal) {
|
||||||
|
var input = literal.value();
|
||||||
|
var list = new ArrayList<TextNode>();
|
||||||
|
var inputLength = input.length();
|
||||||
|
|
||||||
|
var matcher = CODE_REGEX.matcher(input);
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
if (inputLength <= matcher.start()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String betweenText = input.substring(pos, matcher.start());
|
||||||
|
|
||||||
|
if (!betweenText.isEmpty()) {
|
||||||
|
list.add(new LiteralNode(betweenText));
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = matcher.group(1);
|
||||||
|
|
||||||
|
var display = Component.literal(content).withStyle(ChatFormatting.GRAY);
|
||||||
|
var hover = Component.nullToEmpty("Click to copy");
|
||||||
|
|
||||||
|
var text = Component.empty()
|
||||||
|
.append(display)
|
||||||
|
.setStyle(Style.EMPTY
|
||||||
|
.withHoverEvent(
|
||||||
|
new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover)
|
||||||
|
)
|
||||||
|
.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, content))
|
||||||
|
);
|
||||||
|
|
||||||
|
list.add(new DirectTextNode(text));
|
||||||
|
|
||||||
|
pos = matcher.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos < inputLength) {
|
||||||
|
var text = input.substring(pos, inputLength);
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
list.add(new LiteralNode(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.toArray(TextNode[]::new);
|
||||||
|
}
|
||||||
|
return TextNode.array(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package me.alexdevs.solstice.api.text.parser;
|
||||||
|
|
||||||
|
import eu.pb4.placeholders.api.node.DirectTextNode;
|
||||||
|
import eu.pb4.placeholders.api.node.LiteralNode;
|
||||||
|
import eu.pb4.placeholders.api.node.TextNode;
|
||||||
|
import eu.pb4.placeholders.api.node.parent.ParentNode;
|
||||||
|
import eu.pb4.placeholders.api.parsers.NodeParser;
|
||||||
|
import me.alexdevs.solstice.api.text.Format;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import net.minecraft.network.chat.ClickEvent;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.HoverEvent;
|
||||||
|
import net.minecraft.network.chat.Style;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class LinkParser implements NodeParser {
|
||||||
|
public static final Pattern URL_REGEX = Pattern.compile("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TextNode[] parseNodes(TextNode node) {
|
||||||
|
if (node instanceof LiteralNode literalNode) {
|
||||||
|
var input = literalNode.value();
|
||||||
|
var list = new ArrayList<TextNode>();
|
||||||
|
var inputLength = input.length();
|
||||||
|
|
||||||
|
var matcher = URL_REGEX.matcher(input);
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
var config = CoreModule.getConfig();
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
if (inputLength <= matcher.start()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String betweenText = input.substring(pos, matcher.start());
|
||||||
|
|
||||||
|
if (!betweenText.isEmpty()) {
|
||||||
|
list.add(new LiteralNode(betweenText));
|
||||||
|
}
|
||||||
|
|
||||||
|
var link = matcher.group();
|
||||||
|
|
||||||
|
var url = Component.nullToEmpty(link);
|
||||||
|
|
||||||
|
var placeholders = Map.of(
|
||||||
|
"url", url,
|
||||||
|
"label", url
|
||||||
|
);
|
||||||
|
|
||||||
|
var display = Format.parse(
|
||||||
|
config.link,
|
||||||
|
placeholders
|
||||||
|
);
|
||||||
|
|
||||||
|
var hover = Format.parse(
|
||||||
|
config.linkHover,
|
||||||
|
placeholders
|
||||||
|
);
|
||||||
|
|
||||||
|
var text = Component.empty()
|
||||||
|
.append(display)
|
||||||
|
.setStyle(Style.EMPTY
|
||||||
|
.withHoverEvent(
|
||||||
|
new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover)
|
||||||
|
)
|
||||||
|
.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, link))
|
||||||
|
);
|
||||||
|
|
||||||
|
list.add(new DirectTextNode(text));
|
||||||
|
|
||||||
|
pos = matcher.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos < inputLength) {
|
||||||
|
var text = input.substring(pos, inputLength);
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
list.add(new LiteralNode(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.toArray(TextNode[]::new);
|
||||||
|
} else if (node instanceof ParentNode parentNode) {
|
||||||
|
var list = new ArrayList<TextNode>();
|
||||||
|
|
||||||
|
for (var child : parentNode.getChildren()) {
|
||||||
|
list.addAll(List.of(this.parseNodes(child)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextNode[]{
|
||||||
|
parentNode.copyWith(list.toArray(TextNode[]::new))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextNode.array(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package me.alexdevs.solstice.api.text.parser;
|
||||||
|
|
||||||
|
import eu.pb4.placeholders.api.node.TextNode;
|
||||||
|
import eu.pb4.placeholders.api.node.parent.ClickActionNode;
|
||||||
|
import eu.pb4.placeholders.api.node.parent.FormattingNode;
|
||||||
|
import eu.pb4.placeholders.api.node.parent.HoverNode;
|
||||||
|
import me.alexdevs.solstice.api.text.Format;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.ClickEvent;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class MarkdownComponentParser {
|
||||||
|
public static TextNode spoilerFormatting(TextNode[] textNodes) {
|
||||||
|
var text = TextNode.asSingle(textNodes);
|
||||||
|
return new HoverNode<>(
|
||||||
|
TextNode.array(
|
||||||
|
new FormattingNode(TextNode.array(TextNode.of("\u258C".repeat(text.toText().getString().length()))), ChatFormatting.DARK_GRAY)
|
||||||
|
),
|
||||||
|
HoverNode.Action.TEXT, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextNode quoteFormatting(TextNode[] textNodes) {
|
||||||
|
return new ClickActionNode(
|
||||||
|
TextNode.array(
|
||||||
|
new HoverNode<>(
|
||||||
|
TextNode.array(new FormattingNode(textNodes, ChatFormatting.GRAY)),
|
||||||
|
HoverNode.Action.TEXT, TextNode.of("Click to copy"))
|
||||||
|
),
|
||||||
|
ClickEvent.Action.COPY_TO_CLIPBOARD, TextNode.asSingle(textNodes)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextNode urlFormatting(TextNode[] textNodes, TextNode url) {
|
||||||
|
var config = CoreModule.getConfig();
|
||||||
|
var placeholders = Map.of(
|
||||||
|
"label", TextNode.wrap(textNodes).toText(),
|
||||||
|
"url", url.toText()
|
||||||
|
);
|
||||||
|
var text = Format.parse(config.link, placeholders);
|
||||||
|
var hover = Format.parse(config.linkHover, placeholders);
|
||||||
|
|
||||||
|
return new HoverNode<>(TextNode.array(
|
||||||
|
new ClickActionNode(
|
||||||
|
TextNode.array(
|
||||||
|
TextNode.convert(text)
|
||||||
|
),
|
||||||
|
ClickEvent.Action.OPEN_URL, url)),
|
||||||
|
HoverNode.Action.TEXT, TextNode.convert(hover)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package me.alexdevs.solstice.api.text.parser;
|
||||||
|
|
||||||
|
import eu.pb4.placeholders.api.parsers.MarkdownLiteParserV1;
|
||||||
|
import eu.pb4.placeholders.api.parsers.MarkdownLiteParserV1.MarkdownFormat;
|
||||||
|
import eu.pb4.placeholders.api.parsers.NodeParser;
|
||||||
|
|
||||||
|
public class MarkdownParser {
|
||||||
|
public static final MarkdownFormat[] ALL = new MarkdownFormat[]{
|
||||||
|
//MarkdownFormat.QUOTE,
|
||||||
|
MarkdownFormat.BOLD,
|
||||||
|
MarkdownFormat.ITALIC,
|
||||||
|
MarkdownFormat.UNDERLINE,
|
||||||
|
MarkdownFormat.STRIKETHROUGH,
|
||||||
|
MarkdownFormat.SPOILER,
|
||||||
|
MarkdownFormat.URL
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final NodeParser defaultParser = createParser(ALL);
|
||||||
|
|
||||||
|
public static NodeParser createParser(MarkdownFormat[] capabilities) {
|
||||||
|
var mdParser = new MarkdownLiteParserV1(
|
||||||
|
MarkdownComponentParser::spoilerFormatting,
|
||||||
|
MarkdownComponentParser::quoteFormatting,
|
||||||
|
MarkdownComponentParser::urlFormatting,
|
||||||
|
capabilities
|
||||||
|
);
|
||||||
|
|
||||||
|
return NodeParser.merge(new CodeParser(), mdParser, new LinkParser());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package me.alexdevs.solstice.api.text.tag;
|
||||||
|
|
||||||
|
import eu.pb4.placeholders.api.node.TextNode;
|
||||||
|
import eu.pb4.placeholders.api.node.parent.GradientNode;
|
||||||
|
import eu.pb4.placeholders.api.parsers.TextParserV1;
|
||||||
|
import eu.pb4.placeholders.impl.textparser.TextParserImpl;
|
||||||
|
import me.alexdevs.solstice.api.color.Gradient;
|
||||||
|
import me.alexdevs.solstice.api.color.RGBColor;
|
||||||
|
import me.alexdevs.solstice.api.utils.MathUtils;
|
||||||
|
import net.minecraft.network.chat.TextColor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement an improved gradient that can also take in the phase.
|
||||||
|
* </p>
|
||||||
|
* Most of the code is from <a href="https://github.com/KyoriPowered/adventure">Kyori Adventure</a>.
|
||||||
|
*
|
||||||
|
* @see <a href="https://github.com/KyoriPowered/adventure/blob/91afed95abf8e5ee9ee51c355629e94b1a2b1997/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java">Kyori Adventure GradientTag</a>
|
||||||
|
*/
|
||||||
|
public class PhaseGradientTag {
|
||||||
|
|
||||||
|
public static TextParserV1.TextTag createTag() {
|
||||||
|
return TextParserV1.TextTag.of("phase_gradient", List.of("pgr", "sgr"), "gradient", true,
|
||||||
|
(tag, data, input, handlers, endAt) -> {
|
||||||
|
var rawArgs = data.split(":");
|
||||||
|
var out = TextParserImpl.recursiveParsing(input, handlers, endAt);
|
||||||
|
|
||||||
|
double phase = 0;
|
||||||
|
final List<TextColor> textColors;
|
||||||
|
|
||||||
|
var args = Arrays.stream(rawArgs).iterator();
|
||||||
|
|
||||||
|
if (args.hasNext()) {
|
||||||
|
textColors = new ArrayList<>();
|
||||||
|
while (args.hasNext()) {
|
||||||
|
var arg = args.next();
|
||||||
|
if (!args.hasNext()) {
|
||||||
|
final var possiblePhase = MathUtils.parseDouble(arg);
|
||||||
|
if (possiblePhase.isPresent()) {
|
||||||
|
phase = MathUtils.clamp(possiblePhase.get(), -1d, 1d);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedColor = TextColor.parseColor(arg);
|
||||||
|
if (parsedColor.isError()) {
|
||||||
|
textColors.add(TextColor.fromRgb(0));
|
||||||
|
} else {
|
||||||
|
textColors.add(parsedColor.getOrThrow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textColors.size() == 1) {
|
||||||
|
return out.value(GradientNode.colors(textColors, out.nodes()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textColors = List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.value(PhaseGradientTag.smoother(textColors, phase, out.nodes()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GradientNode smoother(List<TextColor> colors, double phase, TextNode... nodes) {
|
||||||
|
if (colors.isEmpty()) {
|
||||||
|
colors.add(TextColor.fromRgb(0xffffff));
|
||||||
|
colors.add(TextColor.fromRgb(0x000000));
|
||||||
|
}
|
||||||
|
if (phase < 0) {
|
||||||
|
phase += 1;
|
||||||
|
Collections.reverse(colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GradientNode(nodes, smoothGradient(colors, phase));
|
||||||
|
}
|
||||||
|
|
||||||
|
static GradientNode.GradientProvider smoothGradient(List<TextColor> colors, double phase) {
|
||||||
|
final var ph = phase * colors.size();
|
||||||
|
return (index, length) -> {
|
||||||
|
var multiplier = length == 1 ? 0 : (double) (colors.size() - 1) / (length - 1);
|
||||||
|
var pos = ((index * multiplier) + ph);
|
||||||
|
var lowUnclamped = (int) Math.floor(pos);
|
||||||
|
|
||||||
|
final int high = (int) Math.ceil(pos) % colors.size();
|
||||||
|
final int low = lowUnclamped % colors.size();
|
||||||
|
|
||||||
|
return Gradient.lerp((float) pos - lowUnclamped, new RGBColor(colors.get(low)), new RGBColor(colors.get(high)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package me.alexdevs.solstice.api.utils;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class MathUtils {
|
||||||
|
public static Optional<Double> parseDouble(String value) {
|
||||||
|
try {
|
||||||
|
return Optional.of(Double.parseDouble(value));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double clamp(double value, double min, double max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package me.alexdevs.solstice.api.utils;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import net.minecraft.server.level.ClientInformation;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PlayerUtils {
|
||||||
|
public static boolean isOnline(UUID uuid) {
|
||||||
|
return Solstice.server.getPlayerList().getPlayer(uuid) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerPlayer loadOfflinePlayer(GameProfile profile) {
|
||||||
|
if (isOnline(profile.getId())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerManager = Solstice.server.getPlayerList();
|
||||||
|
var player = playerManager.getPlayerForLogin(profile, ClientInformation.createDefault());
|
||||||
|
playerManager.load(player);
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveOfflinePlayer(ServerPlayer player) {
|
||||||
|
if (isOnline(player.getUUID())) {
|
||||||
|
Solstice.LOGGER.warn("Tried to save offline player data for a player that is online.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var saveHandler = Solstice.server.playerDataStorage;
|
||||||
|
saveHandler.save(player);
|
||||||
|
Solstice.server.getPlayerList().remove(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package me.alexdevs.solstice.core;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.api.command.TimeSpan;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import me.lucko.fabric.api.permissions.v0.Permissions;
|
||||||
|
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class CooldownManager {
|
||||||
|
private final Map<UUID, Map<String, Integer>> cooldowns = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public CooldownManager() {
|
||||||
|
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
|
||||||
|
var playerUuid = handler.getPlayer().getUUID();
|
||||||
|
cooldowns.computeIfAbsent(playerUuid, k -> new ConcurrentHashMap<>());
|
||||||
|
});
|
||||||
|
|
||||||
|
Solstice.scheduler.scheduleAtFixedRate(this::tickDown, 0, 1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tickDown() {
|
||||||
|
for (var entry : cooldowns.entrySet()) {
|
||||||
|
for (var cdEntry : entry.getValue().entrySet()) {
|
||||||
|
var val = cdEntry.getValue() - 1;
|
||||||
|
if (val <= 0) {
|
||||||
|
entry.getValue().remove(cdEntry.getKey());
|
||||||
|
} else {
|
||||||
|
cdEntry.setValue(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExempt(ServerPlayer player, String node) {
|
||||||
|
return Permissions.check(player, node + ".exempt.cooldown", 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean onCooldown(ServerPlayer player, String node) {
|
||||||
|
if (isExempt(player, node))
|
||||||
|
return false;
|
||||||
|
var uuid = player.getUUID();
|
||||||
|
var cooldown = cooldowns.computeIfAbsent(uuid, k -> new ConcurrentHashMap<>());
|
||||||
|
return cooldown.getOrDefault(node, 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Component getMessage(ServerPlayer player, String node) {
|
||||||
|
var uuid = player.getUUID();
|
||||||
|
var cooldown = cooldowns.computeIfAbsent(uuid, k -> new ConcurrentHashMap<>());
|
||||||
|
var value = cooldown.getOrDefault(node, 0);
|
||||||
|
var locale = Solstice.localeManager.getLocale(CoreModule.ID);
|
||||||
|
return locale.get("~cooldown", Map.of(
|
||||||
|
"timespan", Component.nullToEmpty(TimeSpan.toShortString(value))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and start cooldown if the player is not on cooldown.
|
||||||
|
* @param player Player
|
||||||
|
* @param node Permission node
|
||||||
|
* @param seconds Cooldown seconds
|
||||||
|
* @return Whether to execute
|
||||||
|
*/
|
||||||
|
public boolean trigger(ServerPlayer player, String node, int seconds) {
|
||||||
|
if (onCooldown(player, node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExempt(player, node)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uuid = player.getUUID();
|
||||||
|
var cooldown = cooldowns.get(uuid);
|
||||||
|
cooldown.put(node, seconds);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(ServerPlayer player, String node) {
|
||||||
|
var uuid = player.getUUID();
|
||||||
|
var cooldown = cooldowns.get(uuid);
|
||||||
|
cooldown.remove(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
common/src/main/java/me/alexdevs/solstice/core/Modules.java
Normal file
99
common/src/main/java/me/alexdevs/solstice/core/Modules.java
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package me.alexdevs.solstice.core;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.api.module.ModuleBase;
|
||||||
|
import me.alexdevs.solstice.api.module.ModuleEntrypoint;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||||
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
|
import net.minecraft.commands.CommandBuildContext;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
public class Modules {
|
||||||
|
|
||||||
|
private final HashSet<ModuleBase> modules = new HashSet<>();
|
||||||
|
|
||||||
|
public Modules() {
|
||||||
|
CommandRegistrationCallback.EVENT.register(this::registerCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register() {
|
||||||
|
modules.add(new CoreModule());
|
||||||
|
|
||||||
|
var fabric = FabricLoader.getInstance();
|
||||||
|
var moduleContainers = fabric.getEntrypointContainers("solstice", ModuleEntrypoint.class);
|
||||||
|
for (var container : moduleContainers) {
|
||||||
|
var mod = container.getProvider();
|
||||||
|
var modMeta = mod.getMetadata();
|
||||||
|
Solstice.LOGGER.info("Registering module provider '{}' ({}) v{}", modMeta.getName(), modMeta.getId(), modMeta.getVersion());
|
||||||
|
try {
|
||||||
|
var provider = container.getEntrypoint();
|
||||||
|
var providerModules = provider.register();
|
||||||
|
for (var entry : providerModules) {
|
||||||
|
var moduleId = entry.getId();
|
||||||
|
if (modules.stream().anyMatch(m -> m.getId().equals(moduleId))) {
|
||||||
|
Solstice.LOGGER.warn("Module ID conflict: {}", entry.getId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.add(entry);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Solstice.LOGGER.error("Error registering a module from {}", modMeta.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<? extends ModuleBase> getModules() {
|
||||||
|
return Collections.unmodifiableSet(modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getModule(Class<T> classOfModule) {
|
||||||
|
for (var module : modules) {
|
||||||
|
if (classOfModule.isInstance(module)) {
|
||||||
|
return classOfModule.cast(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<? extends ModuleBase> getEnabledModules() {
|
||||||
|
var set = new HashSet<ModuleBase>();
|
||||||
|
getModules().forEach(module -> {
|
||||||
|
if (module instanceof ModuleBase.Toggleable toggleable) {
|
||||||
|
if (toggleable.isEnabled()) {
|
||||||
|
set.add(module);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
set.add(module);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Collections.unmodifiableSet(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initModules() {
|
||||||
|
var enabledModules = getEnabledModules();
|
||||||
|
for (var module : enabledModules) {
|
||||||
|
try {
|
||||||
|
module.init();
|
||||||
|
} catch (NoSuchMethodError e) {
|
||||||
|
Solstice.LOGGER.error("Legacy module {} does not contain the init method. UPDATE!", module.getId(), e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Solstice.LOGGER.error("Error initializing module {}", module.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandRegistry, Commands.CommandSelection environment) {
|
||||||
|
for (var module : modules) {
|
||||||
|
for (var command : module.getCommands()) {
|
||||||
|
command.register(dispatcher, commandRegistry, environment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package me.alexdevs.solstice.core;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class Scheduler extends ScheduledThreadPoolExecutor {
|
||||||
|
private final ConcurrentLinkedQueue<Runnable> queue;
|
||||||
|
|
||||||
|
public Scheduler(int corePoolSize, ConcurrentLinkedQueue<Runnable> queue) {
|
||||||
|
super(corePoolSize);
|
||||||
|
this.queue = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScheduledFuture<?> scheduleSync(Runnable command, long delay, TimeUnit unit) {
|
||||||
|
return this.schedule(() -> queue.add(command), delay, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScheduledFuture<?> scheduleWithFixedDelaySync(Runnable command, long initialDelay, long delay, TimeUnit unit) {
|
||||||
|
return this.scheduleWithFixedDelay(() -> queue.add(command), initialDelay, delay, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScheduledFuture<?> scheduleAtFixedRateSync(Runnable command, long initialDelay, long period, TimeUnit unit) {
|
||||||
|
return this.scheduleAtFixedRate(() -> queue.add(command), initialDelay, period, unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package me.alexdevs.solstice.core;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.platform.Services;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ToggleableConfig {
|
||||||
|
private static ToggleableConfig instance = null;
|
||||||
|
|
||||||
|
private final Map<String, Boolean> modules = new HashMap<>();
|
||||||
|
private final Path filePath;
|
||||||
|
|
||||||
|
public static ToggleableConfig get() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new ToggleableConfig(Services.PLATFORM.getConfigDir().resolve("modules.conf"));
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleableConfig(Path filePath) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled(String id) {
|
||||||
|
return modules.computeIfAbsent(id, (i) -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
if (!this.filePath.toFile().exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (var br = new BufferedReader(new FileReader(this.filePath.toFile()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
var parts = line.split("=");
|
||||||
|
if (parts.length != 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = parts[0].trim();
|
||||||
|
var value = parts[1].trim();
|
||||||
|
|
||||||
|
var enabled = Boolean.parseBoolean(value);
|
||||||
|
modules.put(key, enabled);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("Error loading toggleable state of modules. Assuming all enabled. " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
var list = modules.entrySet().stream().map(e -> new Entry(e.getKey(), e.getValue())).sorted(Comparator.comparing(Entry::id)).toList();
|
||||||
|
try (var bw = new BufferedWriter(new FileWriter(this.filePath.toFile()))) {
|
||||||
|
for (var entry : list) {
|
||||||
|
bw.write(entry.id() + "=" + entry.enabled());
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("Error saving toggleable state of modules. Assuming all enabled in the next load." + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Entry(String id, boolean enabled) {
|
||||||
|
}
|
||||||
|
}
|
||||||
237
common/src/main/java/me/alexdevs/solstice/core/UserCache.java
Normal file
237
common/src/main/java/me/alexdevs/solstice/core/UserCache.java
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
package me.alexdevs.solstice.core;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.io.Files;
|
||||||
|
import com.google.gson.*;
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import me.alexdevs.solstice.Constants;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason I made this instead of using Minecraft's UserCache is because:
|
||||||
|
* <p>
|
||||||
|
* 1. I do not want to use the API to look up missing profiles, just return an empty value instead.
|
||||||
|
* <p>
|
||||||
|
* 2. Using the API to look up profiles is slow and hangs the server, it's annoying.
|
||||||
|
* <p>
|
||||||
|
* The source file is the original usercache.json and saving is disabled.
|
||||||
|
*/
|
||||||
|
public class UserCache {
|
||||||
|
private final Gson gson = new GsonBuilder()
|
||||||
|
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
|
||||||
|
.disableHtmlEscaping()
|
||||||
|
.excludeFieldsWithoutExposeAnnotation()
|
||||||
|
.create();
|
||||||
|
|
||||||
|
private final Map<String, Entry> byName = Maps.newConcurrentMap();
|
||||||
|
private final Map<UUID, Entry> byUUID = Maps.newConcurrentMap();
|
||||||
|
private final AtomicLong accessCount = new AtomicLong();
|
||||||
|
private final File cacheFile;
|
||||||
|
|
||||||
|
public UserCache(File cacheFile) {
|
||||||
|
this.cacheFile = cacheFile;
|
||||||
|
Lists.reverse(this.load()).forEach(this::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long incrementAndGetAccessCount() {
|
||||||
|
return this.accessCount.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<GameProfile> getByName(String name) {
|
||||||
|
name = name.toLowerCase(Locale.ROOT);
|
||||||
|
var entry = byName.get(name);
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
return Optional.of(entry.getProfile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<GameProfile> getByUUID(UUID uuid) {
|
||||||
|
var entry = byUUID.get(uuid);
|
||||||
|
if (entry == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
return Optional.of(entry.getProfile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllNames() {
|
||||||
|
return ImmutableList.copyOf(this.byName.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(GameProfile profile) {
|
||||||
|
var calendar = Calendar.getInstance();
|
||||||
|
calendar.setTime(new Date());
|
||||||
|
calendar.add(Calendar.MONTH, 1);
|
||||||
|
var date = calendar.getTime();
|
||||||
|
var entry = new Entry(profile, date);
|
||||||
|
this.add(entry);
|
||||||
|
//this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(Entry entry) {
|
||||||
|
var gameProfile = entry.getProfile();
|
||||||
|
entry.setLastAccessed(this.incrementAndGetAccessCount());
|
||||||
|
var name = gameProfile.getName();
|
||||||
|
if (name != null) {
|
||||||
|
this.byName.put(name.toLowerCase(Locale.ROOT), entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
var uuid = gameProfile.getId();
|
||||||
|
if (uuid != null) {
|
||||||
|
byUUID.put(uuid, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonElement entryToJson(Entry entry, DateFormat dateFormat) {
|
||||||
|
JsonObject jsonObject = new JsonObject();
|
||||||
|
jsonObject.addProperty("name", entry.getProfile().getName());
|
||||||
|
UUID uUID = entry.getProfile().getId();
|
||||||
|
jsonObject.addProperty("uuid", uUID == null ? "" : uUID.toString());
|
||||||
|
jsonObject.addProperty("expiresOn", dateFormat.format(entry.getExpirationDate()));
|
||||||
|
return jsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<Entry> entryFromJson(JsonElement json, DateFormat dateFormat) {
|
||||||
|
if (!json.isJsonObject())
|
||||||
|
return Optional.empty();
|
||||||
|
|
||||||
|
var root = json.getAsJsonObject();
|
||||||
|
var nameJson = root.get("name");
|
||||||
|
var uuidJson = root.get("uuid");
|
||||||
|
var expiresJson = root.get("expiresOn");
|
||||||
|
if (nameJson == null || uuidJson == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
var uuid = uuidJson.getAsString();
|
||||||
|
var name = nameJson.getAsString();
|
||||||
|
Date date = null;
|
||||||
|
if (expiresJson != null) {
|
||||||
|
try {
|
||||||
|
date = dateFormat.parse(expiresJson.getAsString());
|
||||||
|
} catch (ParseException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name != null && uuid != null && date != null) {
|
||||||
|
UUID uUID;
|
||||||
|
try {
|
||||||
|
uUID = UUID.fromString(uuid);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(new Entry(new GameProfile(uUID, name), date));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Entry> load() {
|
||||||
|
var list = new ArrayList<Entry>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var reader = Files.newReader(this.cacheFile, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var array = gson.fromJson(reader, JsonArray.class);
|
||||||
|
if (array == null)
|
||||||
|
return list;
|
||||||
|
|
||||||
|
var dateFormat = getDateFormat();
|
||||||
|
array.forEach(json -> entryFromJson(json, dateFormat).ifPresent(list::add));
|
||||||
|
} catch (Exception e) {
|
||||||
|
try {
|
||||||
|
reader.close();
|
||||||
|
} catch (Throwable ee) {
|
||||||
|
e.addSuppressed(ee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader != null)
|
||||||
|
reader.close();
|
||||||
|
|
||||||
|
return list;
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// Do nothing
|
||||||
|
} catch (JsonParseException | IOException e) {
|
||||||
|
Constants.LOG.warn("Failed to load Solstice profile cache {}", this.cacheFile, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save() {
|
||||||
|
var jsonArray = new JsonArray();
|
||||||
|
var dateFormat = getDateFormat();
|
||||||
|
this.getLastAccessedEntries(1000).forEach(entry -> jsonArray.add(entryToJson(entry, dateFormat)));
|
||||||
|
var json = this.gson.toJson(jsonArray);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var writer = Files.newWriter(this.cacheFile, StandardCharsets.UTF_8);
|
||||||
|
try {
|
||||||
|
writer.write(json);
|
||||||
|
} catch (IOException e) {
|
||||||
|
try {
|
||||||
|
writer.close();
|
||||||
|
} catch (IOException ee) {
|
||||||
|
e.addSuppressed(ee);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<Entry> getLastAccessedEntries(int limit) {
|
||||||
|
return ImmutableList.copyOf(this.byUUID.values()).stream()
|
||||||
|
.sorted(Comparator.comparing(Entry::getLastAccessed).reversed())
|
||||||
|
.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateFormat getDateFormat() {
|
||||||
|
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Entry {
|
||||||
|
private final GameProfile profile;
|
||||||
|
final Date expirationDate;
|
||||||
|
private volatile long lastAccessed;
|
||||||
|
|
||||||
|
Entry(GameProfile profile, Date expirationDate) {
|
||||||
|
this.profile = profile;
|
||||||
|
this.expirationDate = expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameProfile getProfile() {
|
||||||
|
return this.profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getExpirationDate() {
|
||||||
|
return this.expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastAccessed(long lastAccessed) {
|
||||||
|
this.lastAccessed = lastAccessed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastAccessed() {
|
||||||
|
return this.lastAccessed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package me.alexdevs.solstice.core;
|
||||||
|
|
||||||
|
public class WarmUpManager {
|
||||||
|
public WarmUpManager() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package me.alexdevs.solstice.core.coreModule;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.api.ServerLocation;
|
||||||
|
import me.alexdevs.solstice.api.events.SolsticeEvents;
|
||||||
|
import me.alexdevs.solstice.api.events.WorldSaveCallback;
|
||||||
|
import me.alexdevs.solstice.api.module.ModuleBase;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.commands.PingCommand;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.commands.ServerStatCommand;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.commands.SolsticeCommand;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.data.CoreConfig;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.data.CoreLocale;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.data.CorePlayerData;
|
||||||
|
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class CoreModule extends ModuleBase {
|
||||||
|
public static final String ID = "core";
|
||||||
|
|
||||||
|
public CoreModule() {
|
||||||
|
super(ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() {
|
||||||
|
Solstice.configManager.registerData(ID, CoreConfig.class, CoreConfig::new);
|
||||||
|
Solstice.localeManager.registerShared(CoreLocale.SHARED);
|
||||||
|
Solstice.localeManager.registerModule(ID, CoreLocale.MODULE);
|
||||||
|
|
||||||
|
Solstice.playerData.registerData(ID, CorePlayerData.class, CorePlayerData::new);
|
||||||
|
|
||||||
|
commands.add(new SolsticeCommand(this));
|
||||||
|
commands.add(new ServerStatCommand(this));
|
||||||
|
commands.add(new PingCommand(this));
|
||||||
|
|
||||||
|
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
|
||||||
|
Solstice.getUserCache().add(handler.getPlayer().getGameProfile());
|
||||||
|
var player = handler.getPlayer();
|
||||||
|
var playerData = Solstice.playerData.get(player).getData(CorePlayerData.class);
|
||||||
|
playerData.username = player.getGameProfile().getName();
|
||||||
|
playerData.lastSeenDate = new Date();
|
||||||
|
playerData.ipAddress = handler.getPlayer().getIpAddress();
|
||||||
|
|
||||||
|
if (playerData.firstJoinedDate == null) {
|
||||||
|
Solstice.LOGGER.info("Player {} joined for the first time!", player.getGameProfile().getName());
|
||||||
|
playerData.firstJoinedDate = new Date();
|
||||||
|
SolsticeEvents.WELCOME.invoker().onWelcome(player, server);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerData.username != null && !playerData.username.equals(player.getGameProfile().getName())) {
|
||||||
|
Solstice.LOGGER.info("Player {} has changed their username from {}", player.getGameProfile().getName(), playerData.username);
|
||||||
|
SolsticeEvents.USERNAME_CHANGE.invoker().onUsernameChange(player, playerData.username);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
|
||||||
|
var playerData = Solstice.playerData.get(handler.getPlayer()).getData(CorePlayerData.class);
|
||||||
|
playerData.lastSeenDate = new Date();
|
||||||
|
playerData.logoffPosition = new ServerLocation(handler.getPlayer());
|
||||||
|
Solstice.scheduler.schedule(() -> {
|
||||||
|
Solstice.playerData.dispose(handler.getPlayer().getUUID());
|
||||||
|
}, 1, TimeUnit.SECONDS);
|
||||||
|
});
|
||||||
|
|
||||||
|
WorldSaveCallback.EVENT.register((server, suppressLogs, flush, force) -> {
|
||||||
|
var uuids = server.getPlayerList().getPlayers().stream().map(Entity::getUUID).toList();
|
||||||
|
Solstice.playerData.disposeMissing(uuids);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CoreConfig getConfig() {
|
||||||
|
return Solstice.configManager.getData(CoreConfig.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CorePlayerData getPlayerData(UUID uuid) {
|
||||||
|
return Solstice.playerData.get(uuid).getData(CorePlayerData.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getUsername(UUID uuid) {
|
||||||
|
var profile = Solstice.server.getProfileCache().get(uuid);
|
||||||
|
if(profile.isPresent())
|
||||||
|
return profile.get().getName();
|
||||||
|
|
||||||
|
return uuid.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package me.alexdevs.solstice.core.coreModule.commands;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import me.alexdevs.solstice.api.module.ModCommand;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.commands.arguments.EntityArgument;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class PingCommand extends ModCommand<CoreModule> {
|
||||||
|
public PingCommand(CoreModule module) {
|
||||||
|
super(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getNames() {
|
||||||
|
return List.of("ping");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LiteralArgumentBuilder<CommandSourceStack> command(String name) {
|
||||||
|
return Commands.literal(name)
|
||||||
|
.requires(require("ping.base", true))
|
||||||
|
.executes(context -> {
|
||||||
|
var player = context.getSource().getPlayerOrException();
|
||||||
|
var ping = player.connection.latency();
|
||||||
|
var map = Map.of(
|
||||||
|
"ping", Component.nullToEmpty(String.valueOf(ping))
|
||||||
|
);
|
||||||
|
context.getSource().sendSuccess(() -> module.locale().get("ping.self", map), false);
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.then(Commands.argument("player", EntityArgument.player())
|
||||||
|
.requires(require("ping.others", 1))
|
||||||
|
.executes(context -> {
|
||||||
|
var player = EntityArgument.getPlayer(context, "player");
|
||||||
|
var ping = player.connection.latency();
|
||||||
|
var map = Map.of(
|
||||||
|
"ping", Component.nullToEmpty(String.valueOf(ping)),
|
||||||
|
"player", player.getName()
|
||||||
|
);
|
||||||
|
context.getSource().sendSuccess(() -> module.locale().get("ping.other", map), false);
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package me.alexdevs.solstice.core.coreModule.commands;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import eu.pb4.placeholders.api.PlaceholderContext;
|
||||||
|
import me.alexdevs.solstice.api.command.TimeSpan;
|
||||||
|
import me.alexdevs.solstice.api.module.ModCommand;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ServerStatCommand extends ModCommand<CoreModule> {
|
||||||
|
public ServerStatCommand(CoreModule module) {
|
||||||
|
super(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getNames() {
|
||||||
|
return List.of("serverstat", "tps");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LiteralArgumentBuilder<CommandSourceStack> command(String name) {
|
||||||
|
return Commands.literal(name)
|
||||||
|
.requires(require("serverstat", 3))
|
||||||
|
.executes(context -> {
|
||||||
|
var locale = module.locale();
|
||||||
|
var placeholderContext = PlaceholderContext.of(context.getSource());
|
||||||
|
|
||||||
|
var messages = new ArrayList<Component>();
|
||||||
|
|
||||||
|
messages.add(locale.get("stat.tps", placeholderContext));
|
||||||
|
|
||||||
|
var uptime = Duration.ofMillis(ManagementFactory.getRuntimeMXBean().getUptime());
|
||||||
|
var uptimeFormatted = TimeSpan.toShortString((int)uptime.getSeconds());
|
||||||
|
messages.add(locale.get("stat.uptime", placeholderContext, Map.of(
|
||||||
|
"uptime", Component.nullToEmpty(uptimeFormatted)
|
||||||
|
)));
|
||||||
|
|
||||||
|
var maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
|
||||||
|
var allocatedMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
|
||||||
|
var freeMemory = Runtime.getRuntime().freeMemory() / 1024 / 1024;
|
||||||
|
|
||||||
|
messages.add(locale.get("stat.maxMemory", placeholderContext, Map.of(
|
||||||
|
"memory", Component.nullToEmpty(String.valueOf(maxMemory)),
|
||||||
|
"hover", locale.get("stat.maxMemory.hover")
|
||||||
|
)));
|
||||||
|
|
||||||
|
messages.add(locale.get("stat.dedicatedMemory", placeholderContext, Map.of(
|
||||||
|
"memory", Component.nullToEmpty(String.valueOf(allocatedMemory)),
|
||||||
|
"hover", locale.get("stat.dedicatedMemory.hover")
|
||||||
|
|
||||||
|
)));
|
||||||
|
|
||||||
|
messages.add(locale.get("stat.freeMemory", placeholderContext, Map.of(
|
||||||
|
"memory", Component.nullToEmpty(String.valueOf(freeMemory)),
|
||||||
|
"hover", locale.get("stat.freeMemory.hover")
|
||||||
|
)));
|
||||||
|
|
||||||
|
var text = Component.empty();
|
||||||
|
text.append(locale.get("stat.title"));
|
||||||
|
|
||||||
|
for(var message : messages) {
|
||||||
|
text.append(Component.nullToEmpty("\n"));
|
||||||
|
text.append(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getSource().sendSuccess(() -> text, false);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package me.alexdevs.solstice.core.coreModule.commands;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import me.alexdevs.solstice.api.events.SolsticeEvents;
|
||||||
|
import me.alexdevs.solstice.api.module.Debug;
|
||||||
|
import me.alexdevs.solstice.api.module.ModCommand;
|
||||||
|
import me.alexdevs.solstice.core.coreModule.CoreModule;
|
||||||
|
import me.alexdevs.solstice.api.text.Format;
|
||||||
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.network.chat.ClickEvent;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.HoverEvent;
|
||||||
|
import net.minecraft.network.chat.Style;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static net.minecraft.commands.Commands.literal;
|
||||||
|
|
||||||
|
public class SolsticeCommand extends ModCommand<CoreModule> {
|
||||||
|
public SolsticeCommand(CoreModule module) {
|
||||||
|
super(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getNames() {
|
||||||
|
return List.of("solstice", "sol");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LiteralArgumentBuilder<CommandSourceStack> command(String name) {
|
||||||
|
return literal(name)
|
||||||
|
.requires(require(true))
|
||||||
|
.executes(context -> {
|
||||||
|
var modContainer = FabricLoader.getInstance().getModContainer(Solstice.MOD_ID).orElse(null);
|
||||||
|
if (modContainer == null) {
|
||||||
|
context.getSource().sendSuccess(() -> Component.nullToEmpty("Could not find self in mod list???"), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = modContainer.getMetadata();
|
||||||
|
var placeholders = Map.of(
|
||||||
|
"name", Component.nullToEmpty(metadata.getName()),
|
||||||
|
"version", Component.nullToEmpty(metadata.getVersion().getFriendlyString())
|
||||||
|
);
|
||||||
|
|
||||||
|
var text = Format.parse(
|
||||||
|
"<gold>${name} v${version}</gold>",
|
||||||
|
placeholders);
|
||||||
|
context.getSource().sendSuccess(() -> text, false);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.then(literal("reload")
|
||||||
|
.requires(require("reload", 3))
|
||||||
|
.executes(context -> {
|
||||||
|
try {
|
||||||
|
Solstice.configManager.loadData(true);
|
||||||
|
Solstice.localeManager.reload();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Solstice.LOGGER.error("Failed to reload Solstice", e);
|
||||||
|
context.getSource().sendSuccess(() -> Component.nullToEmpty("Failed to load Solstice config. Check console for more info."), true);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SolsticeEvents.RELOAD.invoker().onReload(Solstice.getInstance());
|
||||||
|
|
||||||
|
context.getSource().sendSuccess(() -> Component.nullToEmpty("Reloaded Solstice config"), true);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
.then(literal("debug")
|
||||||
|
.requires(require("debug", 4))
|
||||||
|
.then(literal("gen-command-list")
|
||||||
|
.executes(context -> {
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
var list = new ArrayList<>(Debug.commandDebugList);
|
||||||
|
|
||||||
|
list.sort(Comparator.comparing(Debug.CommandDebug::module));
|
||||||
|
|
||||||
|
builder.append(String.format("| %s | %s | %s | %s |\n", "Module", "Command", "Aliases", "Permission"));
|
||||||
|
builder.append("|---|---|---|---|\n");
|
||||||
|
for (var command : list) {
|
||||||
|
builder.append(String.format("| %s | %s | %s | %s |\n", command.module(), command.command(), String.join(" ", command.commands()), command.permission()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = builder.toString();
|
||||||
|
|
||||||
|
var file = FabricLoader.getInstance().getGameDir().resolve("solstice-commands.md").toFile();
|
||||||
|
try (var fw = new FileWriter(file)) {
|
||||||
|
fw.write(output);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new SimpleCommandExceptionType(Component.nullToEmpty(e.getMessage())).create();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getSource().sendSuccess(() -> Component.nullToEmpty("Generated 'solstice-commands.md'"), true);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
.then(literal("tags")
|
||||||
|
.executes(context -> {
|
||||||
|
var player = context.getSource().getPlayerOrException();
|
||||||
|
|
||||||
|
var hand = player.getUsedItemHand();
|
||||||
|
var itemStack = player.getItemInHand(hand);
|
||||||
|
|
||||||
|
var entry = itemStack.getItemHolder().unwrapKey().get();
|
||||||
|
var entryString = String.format("Tags for [%s / %s]:", entry.registry(), entry.location());
|
||||||
|
|
||||||
|
var text = Component.empty();
|
||||||
|
text.append(Component.nullToEmpty(entryString));
|
||||||
|
var tags = itemStack.getTags().iterator();
|
||||||
|
while(tags.hasNext()) {
|
||||||
|
var tag = tags.next();
|
||||||
|
text.append(Component.nullToEmpty("\n"));
|
||||||
|
text.append(
|
||||||
|
Component.literal(" #" + tag.location())
|
||||||
|
.setStyle(Style.EMPTY
|
||||||
|
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.nullToEmpty("Click to copy")))
|
||||||
|
.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "#" + tag.location()))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getSource().sendSuccess(() -> text, false);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package me.alexdevs.solstice.core.coreModule.data;
|
||||||
|
|
||||||
|
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||||||
|
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||||
|
|
||||||
|
@ConfigSerializable
|
||||||
|
public class CoreConfig {
|
||||||
|
@Comment("Generic date format to use.\nMetric format: dd/MM/yyyy\nUSA format: MM/dd/yyyy")
|
||||||
|
public String dateFormat = "dd/MM/yyyy";
|
||||||
|
|
||||||
|
@Comment("Generic time format to use.\n24h format: HH:mm\n12h format: hh:mm a")
|
||||||
|
public String timeFormat = "HH:mm";
|
||||||
|
|
||||||
|
@Comment("Generic date + time format to use.")
|
||||||
|
public String dateTimeFormat = "dd/MM/yyyy HH:mm";
|
||||||
|
|
||||||
|
@Comment("Format to use when displaying links in chat.")
|
||||||
|
public String link = "<c:#8888ff><u>${label}</u></c>";
|
||||||
|
|
||||||
|
@Comment("Format to use when hovering over the link in chat.")
|
||||||
|
public String linkHover = "${url}";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package me.alexdevs.solstice.core.coreModule.data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class CoreLocale {
|
||||||
|
public static final Map<String, String> SHARED = Map.ofEntries(
|
||||||
|
Map.entry("button", "<click:run_command:'${command}'><hover:show_text:'${hoverText}'><aqua>[</aqua>${label}<aqua>]</aqua></hover></click>"),
|
||||||
|
Map.entry("buttonSuggest", "<click:suggest_command:'${command}'><hover:show_text:'${hoverText}'><aqua>[</aqua>${label}<aqua>]</aqua></hover></click>"),
|
||||||
|
Map.entry("accept", "<green>Accept</green>"),
|
||||||
|
Map.entry("refuse", "<red>Refuse</red>"),
|
||||||
|
Map.entry("accept.hover", "Click to accept"),
|
||||||
|
Map.entry("refuse.hover", "Click to refuse"),
|
||||||
|
Map.entry("tooManyTargets", "<red>The provided selector contains too many targets.</red>"),
|
||||||
|
Map.entry("cooldown", "<gold>You are on cooldown for <yellow>${timespan}</yellow>.</gold>"),
|
||||||
|
Map.entry("unit.second", "${n} second"),
|
||||||
|
Map.entry("unit.seconds", "${n} seconds"),
|
||||||
|
Map.entry("unit.minute", "${n} minute"),
|
||||||
|
Map.entry("unit.minutes", "${n} minutes"),
|
||||||
|
Map.entry("unit.hour", "${n} hour"),
|
||||||
|
Map.entry("unit.hours", "${n} hours"),
|
||||||
|
Map.entry("unit.day", "${n} day"),
|
||||||
|
Map.entry("unit.days", "${n} days"),
|
||||||
|
Map.entry("unit.week", "${n} week"),
|
||||||
|
Map.entry("unit.weeks", "${n} weeks"),
|
||||||
|
Map.entry("unit.month", "${n} month"),
|
||||||
|
Map.entry("unit.months", "${n} months"),
|
||||||
|
Map.entry("unit.year", "${n} year"),
|
||||||
|
Map.entry("unit.years", "${n} years")
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final Map<String, String> MODULE = Map.ofEntries(
|
||||||
|
Map.entry("stat.title", "<gold>Server Statistics</gold>"),
|
||||||
|
Map.entry("stat.tps", "<gold>Current TPS: %server:tps_colored%<gray>/20.0</gray></gold>"),
|
||||||
|
Map.entry("stat.uptime", "<gold>Server Uptime: <yellow>${uptime}</yellow></gold>"),
|
||||||
|
Map.entry("stat.maxMemory", "<hover:'${hover}'><gold>Maximum memory: <yellow>${memory} MB</yellow></gold></hover>"),
|
||||||
|
Map.entry("stat.maxMemory.hover", "How much memory the JVM can take at most in the system."),
|
||||||
|
Map.entry("stat.dedicatedMemory", "<hover:'${hover}'><gold>Dedicated memory: <yellow>${memory} MB</yellow></gold></hover>"),
|
||||||
|
Map.entry("stat.dedicatedMemory.hover", "How much memory the JVM is using, can expand up to maximum memory."),
|
||||||
|
Map.entry("stat.freeMemory", "<hover:'${hover}'><gold>Free memory: <yellow>${memory} MB</yellow></gold></hover>"),
|
||||||
|
Map.entry("stat.freeMemory.hover", "How much memory is left free in the dedicated memory."),
|
||||||
|
Map.entry("ping.self", "<gold>Ping: <yellow>${ping}ms</yellow></gold>"),
|
||||||
|
Map.entry("ping.other", "<gold><yellow>${player}</yellow>'s ping: <yellow>${ping}ms</yellow></gold>")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package me.alexdevs.solstice.core.coreModule.data;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.api.ServerLocation;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class CorePlayerData {
|
||||||
|
public String username;
|
||||||
|
public @Nullable Date firstJoinedDate;
|
||||||
|
public @Nullable Date lastSeenDate;
|
||||||
|
public @Nullable String ipAddress;
|
||||||
|
public @Nullable ServerLocation logoffPosition = null;
|
||||||
|
}
|
||||||
137
common/src/main/java/me/alexdevs/solstice/data/PlayerData.java
Normal file
137
common/src/main/java/me/alexdevs/solstice/data/PlayerData.java
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
package me.alexdevs.solstice.data;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import me.alexdevs.solstice.Constants;
|
||||||
|
import net.minecraft.Util;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class PlayerData {
|
||||||
|
protected final UUID uuid;
|
||||||
|
protected final Path filePath;
|
||||||
|
protected final Path basePath;
|
||||||
|
|
||||||
|
protected final Map<String, Class<?>> classMap = new HashMap<>();
|
||||||
|
protected final Map<Class<?>, Object> data = new HashMap<>();
|
||||||
|
protected final Map<Class<?>, Supplier<?>> providers = new HashMap<>();
|
||||||
|
protected final Gson gson = new GsonBuilder()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.disableHtmlEscaping()
|
||||||
|
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX")
|
||||||
|
.serializeNulls()
|
||||||
|
.create();
|
||||||
|
protected JsonObject node;
|
||||||
|
|
||||||
|
public PlayerData(Path basePath, UUID uuid, Map<String, Class<?>> classMap, Map<Class<?>, Supplier<?>> providers) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.classMap.putAll(classMap);
|
||||||
|
this.providers.putAll(providers);
|
||||||
|
this.basePath = basePath;
|
||||||
|
this.filePath = basePath.resolve(uuid + ".json");
|
||||||
|
|
||||||
|
loadData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getDataPath() {
|
||||||
|
return this.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getData(Class<T> clazz) {
|
||||||
|
if (this.data.containsKey(clazz))
|
||||||
|
return (T) this.data.get(clazz);
|
||||||
|
|
||||||
|
if (this.providers.containsKey(clazz)) {
|
||||||
|
final T result = (T) this.providers.get(clazz).get();
|
||||||
|
this.data.put(clazz, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException(clazz.getSimpleName() + " does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
for (var entry : classMap.entrySet()) {
|
||||||
|
var obj = data.get(entry.getValue());
|
||||||
|
node.add(entry.getKey(), gson.toJsonTree(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentDir = filePath.getParent();
|
||||||
|
var fileName = filePath.getFileName().toString();
|
||||||
|
|
||||||
|
if (parentDir.toFile().mkdirs()) {
|
||||||
|
Constants.LOG.debug("Players data directory created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var temp = File.createTempFile(uuid.toString() + "-", ".json", parentDir.toFile());
|
||||||
|
var tempWriter = new FileWriter(temp);
|
||||||
|
gson.toJson(node, tempWriter);
|
||||||
|
tempWriter.close();
|
||||||
|
|
||||||
|
var target = filePath;
|
||||||
|
var backup = parentDir.resolve(fileName + "_old");
|
||||||
|
Util.safeReplaceFile(target, temp.toPath(), backup);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Constants.LOG.error("Could not save {}. This will lead to data loss!", filePath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void registerData(String id, Class<T> clazz, Supplier<T> creator) {
|
||||||
|
classMap.put(id, clazz);
|
||||||
|
providers.put(clazz, creator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadData(boolean force) {
|
||||||
|
if (node == null || force) {
|
||||||
|
node = loadNode();
|
||||||
|
}
|
||||||
|
data.clear();
|
||||||
|
|
||||||
|
for (var entry : classMap.entrySet()) {
|
||||||
|
data.put(entry.getValue(), get(node.get(entry.getKey()), entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JsonObject loadNode() {
|
||||||
|
if (!this.filePath.toFile().exists())
|
||||||
|
return new JsonObject();
|
||||||
|
try (var fr = new FileReader(this.filePath.toFile())) {
|
||||||
|
var reader = gson.newJsonReader(fr);
|
||||||
|
return JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Constants.LOG.error("Could not load player data of UUID {}!", uuid, e);
|
||||||
|
safeMove();
|
||||||
|
return new JsonObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void safeMove() {
|
||||||
|
var df = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||||
|
var date = df.format(new Date());
|
||||||
|
var newPath = basePath.resolve(String.format("%s.%s.json", uuid, date));
|
||||||
|
if (filePath.toFile().renameTo(newPath.toFile())) {
|
||||||
|
Constants.LOG.warn("{} has been renamed to {}!", filePath, newPath);
|
||||||
|
} else {
|
||||||
|
Constants.LOG.error("Could not move file {}. Solstice cannot safely manage player data.", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <T> T get(@Nullable JsonElement node, Class<T> clazz) {
|
||||||
|
if (node == null)
|
||||||
|
return (T) providers.get(clazz).get();
|
||||||
|
return gson.fromJson(node, clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package me.alexdevs.solstice.data;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import me.alexdevs.solstice.Constants;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class PlayerDataManager {
|
||||||
|
private final Map<String, Class<?>> classMap = new HashMap<>();
|
||||||
|
private final Map<Class<?>, Supplier<?>> providers = new HashMap<>();
|
||||||
|
private final Map<UUID, PlayerData> playerData = new ConcurrentHashMap<>();
|
||||||
|
private Path basePath;
|
||||||
|
|
||||||
|
public Path getDataPath() {
|
||||||
|
return basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataPath(Path basePath) {
|
||||||
|
this.basePath = basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register data model for the player
|
||||||
|
*
|
||||||
|
* @param id Module key in the data
|
||||||
|
* @param clazz Class of data
|
||||||
|
* @param creator Default values provider
|
||||||
|
* @param <T> Type of class of data
|
||||||
|
*/
|
||||||
|
public <T> void registerData(String id, Class<T> clazz, Supplier<T> creator) {
|
||||||
|
classMap.put(id, clazz);
|
||||||
|
providers.put(clazz, creator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data of a player. Will load if not loaded.
|
||||||
|
*
|
||||||
|
* @param uuid Player UUID
|
||||||
|
* @return player data
|
||||||
|
*/
|
||||||
|
public PlayerData get(UUID uuid) {
|
||||||
|
if (!playerData.containsKey(uuid)) {
|
||||||
|
return load(uuid);
|
||||||
|
}
|
||||||
|
return playerData.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data of a player. Will load if not loaded.
|
||||||
|
*
|
||||||
|
* @param player Player
|
||||||
|
* @return player data
|
||||||
|
*/
|
||||||
|
public PlayerData get(ServerPlayer player) {
|
||||||
|
return get(player.getUUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data of a player. Will load if not loaded.
|
||||||
|
*
|
||||||
|
* @param profile Player profile
|
||||||
|
* @return player data
|
||||||
|
*/
|
||||||
|
public PlayerData get(GameProfile profile) {
|
||||||
|
return get(profile.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save player data and unload from memory
|
||||||
|
*
|
||||||
|
* @param uuid Player UUID
|
||||||
|
*/
|
||||||
|
public void dispose(UUID uuid) {
|
||||||
|
if (playerData.containsKey(uuid)) {
|
||||||
|
Constants.LOG.debug("Unloading player data {}", uuid);
|
||||||
|
var data = playerData.remove(uuid);
|
||||||
|
data.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disposeMissing(List<UUID> uuids) {
|
||||||
|
for(var entry : playerData.entrySet()) {
|
||||||
|
if(!uuids.contains(entry.getKey())) {
|
||||||
|
dispose(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayerData load(UUID uuid) {
|
||||||
|
Constants.LOG.debug("Loading player data {}", uuid);
|
||||||
|
var data = new PlayerData(this.basePath, uuid, classMap, providers);
|
||||||
|
playerData.put(uuid, data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save all player data without disposing.
|
||||||
|
*/
|
||||||
|
public void saveAll() {
|
||||||
|
if (!this.basePath.toFile().exists()) {
|
||||||
|
this.basePath.toFile().mkdirs();
|
||||||
|
}
|
||||||
|
for (var entry : playerData.entrySet()) {
|
||||||
|
var data = entry.getValue();
|
||||||
|
data.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
common/src/main/java/me/alexdevs/solstice/data/ServerData.java
Normal file
124
common/src/main/java/me/alexdevs/solstice/data/ServerData.java
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
package me.alexdevs.solstice.data;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import me.alexdevs.solstice.Constants;
|
||||||
|
import net.minecraft.Util;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class ServerData {
|
||||||
|
protected final Map<String, Class<?>> classMap = new HashMap<>();
|
||||||
|
protected final Map<Class<?>, Object> data = new HashMap<>();
|
||||||
|
protected final Map<Class<?>, Supplier<?>> providers = new HashMap<>();
|
||||||
|
protected final Gson gson = new GsonBuilder()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.disableHtmlEscaping()
|
||||||
|
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX")
|
||||||
|
.serializeNulls()
|
||||||
|
.create();
|
||||||
|
protected Path filePath;
|
||||||
|
protected JsonObject node;
|
||||||
|
|
||||||
|
public Path getDataPath() {
|
||||||
|
return this.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataPath(Path filePath) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getData(Class<T> clazz) {
|
||||||
|
if (this.data.containsKey(clazz))
|
||||||
|
return (T) this.data.get(clazz);
|
||||||
|
|
||||||
|
if (this.providers.containsKey(clazz)) {
|
||||||
|
final T result = (T) this.providers.get(clazz).get();
|
||||||
|
this.data.put(clazz, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException(clazz.getSimpleName() + " does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
for (var entry : classMap.entrySet()) {
|
||||||
|
var obj = data.get(entry.getValue());
|
||||||
|
node.add(entry.getKey(), gson.toJsonTree(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentDir = filePath.getParent();
|
||||||
|
var fileName = filePath.getFileName().toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var temp = File.createTempFile("server-", ".json", parentDir.toFile());
|
||||||
|
var tempWriter = new FileWriter(temp);
|
||||||
|
gson.toJson(node, tempWriter);
|
||||||
|
tempWriter.close();
|
||||||
|
|
||||||
|
var target = filePath;
|
||||||
|
var backup = parentDir.resolve(fileName + "_old");
|
||||||
|
Util.safeReplaceFile(target, temp.toPath(), backup);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Constants.LOG.error("Could not save {}. This will lead to data loss!", filePath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void registerData(String id, Class<T> clazz, Supplier<T> creator) {
|
||||||
|
classMap.put(id, clazz);
|
||||||
|
providers.put(clazz, creator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadData(boolean force) {
|
||||||
|
if (node == null || force) {
|
||||||
|
node = loadNode();
|
||||||
|
}
|
||||||
|
data.clear();
|
||||||
|
|
||||||
|
for (var entry : classMap.entrySet()) {
|
||||||
|
data.put(entry.getValue(), get(node.get(entry.getKey()), entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JsonObject loadNode() {
|
||||||
|
if (!this.filePath.toFile().exists())
|
||||||
|
return new JsonObject();
|
||||||
|
try (var fr = new FileReader(this.filePath.toFile())) {
|
||||||
|
var reader = gson.newJsonReader(fr);
|
||||||
|
return JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Constants.LOG.error("Could not load server data!", e);
|
||||||
|
safeMove();
|
||||||
|
return new JsonObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void safeMove() {
|
||||||
|
var df = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||||
|
var date = df.format(new Date());
|
||||||
|
var basePath = filePath.getParent();
|
||||||
|
var newPath = basePath.resolve(String.format("server.%s.json", date));
|
||||||
|
if (filePath.toFile().renameTo(newPath.toFile())) {
|
||||||
|
Constants.LOG.warn("{} has been renamed to {}!", filePath, newPath);
|
||||||
|
} else {
|
||||||
|
Constants.LOG.error("Could not move file {}. Solstice cannot safely manage server data.", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <T> T get(@Nullable JsonElement node, Class<T> clazz) {
|
||||||
|
if (node == null)
|
||||||
|
return (T) providers.get(clazz).get();
|
||||||
|
return gson.fromJson(node, clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
package me.alexdevs.solstice.integrations;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.Solstice;
|
||||||
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||||
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
|
import net.luckperms.api.LuckPerms;
|
||||||
|
import net.luckperms.api.LuckPermsProvider;
|
||||||
|
import net.luckperms.api.event.user.UserDataRecalculateEvent;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class LuckPermsIntegration {
|
||||||
|
|
||||||
|
private static LuckPerms luckPerms;
|
||||||
|
private static boolean available = false;
|
||||||
|
|
||||||
|
private static final Map<UUID, Optional<String>> prefixMap = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<UUID, Optional<String>> suffixMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
if (!isAvailable()) {
|
||||||
|
Solstice.LOGGER.warn("LuckPerms not available! It is recommended to install LuckPerms to configure permissions and groups.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var container = FabricLoader.getInstance().getModContainer(Solstice.MOD_ID).get();
|
||||||
|
|
||||||
|
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
|
||||||
|
luckPerms = LuckPermsProvider.get();
|
||||||
|
available = true;
|
||||||
|
var eventBus = luckPerms.getEventBus();
|
||||||
|
|
||||||
|
eventBus.subscribe(container, UserDataRecalculateEvent.class, Listeners::onDataRecalculate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAvailable() {
|
||||||
|
return FabricLoader.getInstance().isModLoaded("luckperms");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable String getPrefix(ServerPlayer player) {
|
||||||
|
if (!available) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefixMap.computeIfAbsent(player.getUUID(), uuid -> {
|
||||||
|
try {
|
||||||
|
var playerMeta = luckPerms.getPlayerAdapter(ServerPlayer.class).getMetaData(player);
|
||||||
|
return Optional.ofNullable(playerMeta.getPrefix());
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// Fake player may throw with IllegalStateException
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable String getSuffix(ServerPlayer player) {
|
||||||
|
if (!available) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return suffixMap.computeIfAbsent(player.getUUID(), uuid -> {
|
||||||
|
try {
|
||||||
|
var playerMeta = luckPerms.getPlayerAdapter(ServerPlayer.class).getMetaData(player);
|
||||||
|
return Optional.ofNullable(playerMeta.getSuffix());
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// Fake player may throw with IllegalStateException
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInGroup(ServerPlayer player, String group) {
|
||||||
|
if (!available) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var user = luckPerms.getPlayerAdapter(ServerPlayer.class).getUser(player);
|
||||||
|
var inheritedGroups = user.getInheritedGroups(user.getQueryOptions());
|
||||||
|
return inheritedGroups.stream().anyMatch(g -> g.getName().equalsIgnoreCase(group));
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// Fake player may throw with IllegalStateException
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Listeners {
|
||||||
|
public static void onDataRecalculate(UserDataRecalculateEvent event) {
|
||||||
|
var uuid = event.getUser().getUniqueId();
|
||||||
|
prefixMap.remove(uuid);
|
||||||
|
suffixMap.remove(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package me.alexdevs.solstice.integrations;
|
||||||
|
|
||||||
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
|
|
||||||
|
public class TrinketsIntegration {
|
||||||
|
public static boolean isAvailable() {
|
||||||
|
return FabricLoader.getInstance().isModLoaded("trinkets");
|
||||||
|
}
|
||||||
|
}
|
||||||
52
common/src/main/java/me/alexdevs/solstice/locale/Locale.java
Normal file
52
common/src/main/java/me/alexdevs/solstice/locale/Locale.java
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package me.alexdevs.solstice.locale;
|
||||||
|
|
||||||
|
//import eu.pb4.placeholders.api.PlaceholderContext;
|
||||||
|
import me.alexdevs.solstice.api.text.Format;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class Locale {
|
||||||
|
public final String id;
|
||||||
|
|
||||||
|
private final Supplier<LocaleManager.LocaleModel> localeSupplier;
|
||||||
|
|
||||||
|
public Locale(String id, Supplier<LocaleManager.LocaleModel> localeSupplier) {
|
||||||
|
this.id = id;
|
||||||
|
this.localeSupplier = localeSupplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String raw(String path) {
|
||||||
|
String fullPath;
|
||||||
|
if (path.startsWith("~")) {
|
||||||
|
fullPath = "shared." + path.substring(1);
|
||||||
|
} else if (path.startsWith("/")) {
|
||||||
|
fullPath = path.substring(1);
|
||||||
|
} else {
|
||||||
|
fullPath = "module." + this.id + "." + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return localeSupplier.get().get(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Component get(String path) {
|
||||||
|
var src = this.raw(path);
|
||||||
|
return Format.parse(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Component get(String path, PlaceholderContext context) {
|
||||||
|
var src = this.raw(path);
|
||||||
|
return Format.parse(src, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Component get(String path, Map<String, Component> placeholders) {
|
||||||
|
var src = this.raw(path);
|
||||||
|
return Format.parse(src, placeholders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Component get(String path, PlaceholderContext context, Map<String, Component> placeholders) {
|
||||||
|
var src = this.raw(path);
|
||||||
|
return Format.parse(src, context, placeholders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
package me.alexdevs.solstice.locale;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import me.alexdevs.solstice.Constants;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class LocaleManager {
|
||||||
|
private static final Gson gson = new GsonBuilder()
|
||||||
|
.disableHtmlEscaping()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX")
|
||||||
|
.create();
|
||||||
|
private static final Pattern sharedRegex = Pattern.compile("^shared\\.(.+)$");
|
||||||
|
private static final Pattern moduleRegex = Pattern.compile("^module\\.(\\w+)\\.(.+)$");
|
||||||
|
private final Path path;
|
||||||
|
private final TypeToken<?> oldType = TypeToken.getParameterized(Map.class, String.class, String.class);
|
||||||
|
private final LocaleModel defaultMap = new LocaleModel();
|
||||||
|
private LocaleModel locale;
|
||||||
|
|
||||||
|
|
||||||
|
public LocaleManager(Path path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable LocalePath getPath(String fullPath) {
|
||||||
|
var matcher = sharedRegex.matcher(fullPath);
|
||||||
|
if (matcher.find()) {
|
||||||
|
var key = matcher.group(1);
|
||||||
|
return new LocalePath(LocaleType.SHARED, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
matcher = moduleRegex.matcher(fullPath);
|
||||||
|
if (matcher.find()) {
|
||||||
|
var moduleId = matcher.group(1);
|
||||||
|
var key = matcher.group(2);
|
||||||
|
return new LocalePath(LocaleType.MODULE, key, moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Locale getLocale(String id) {
|
||||||
|
return new Locale(id, () -> locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerModule(String id, Map<String, String> defaults) {
|
||||||
|
this.defaultMap.modules.put(id, new ConcurrentHashMap<>(defaults));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerShared(Map<String, String> defaults) {
|
||||||
|
this.defaultMap.shared.putAll(defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() throws IOException {
|
||||||
|
if (!path.toFile().exists()) {
|
||||||
|
locale = new LocaleModel();
|
||||||
|
prepare();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var bf = new BufferedReader(new FileReader(path.toFile(), StandardCharsets.UTF_8));
|
||||||
|
locale = gson.fromJson(bf, LocaleModel.class);
|
||||||
|
bf.close();
|
||||||
|
|
||||||
|
if (locale.shared.isEmpty() && locale.modules.isEmpty()) {
|
||||||
|
Constants.LOG.warn("Locale casting failure. Attempting migration...");
|
||||||
|
migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() throws IOException {
|
||||||
|
var fw = new FileWriter(path.toFile(), StandardCharsets.UTF_8);
|
||||||
|
gson.toJson(locale, fw);
|
||||||
|
fw.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepare() {
|
||||||
|
if (locale == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
defaultMap.shared.forEach((key, value) -> locale.shared.putIfAbsent(key, value));
|
||||||
|
|
||||||
|
//defaultMap.modules.forEach((id, map) -> locale.modules.putIfAbsent(id, new ConcurrentHashMap<>()));
|
||||||
|
for (var defaultMods : defaultMap.modules.entrySet()) {
|
||||||
|
var module = locale.modules.computeIfAbsent(defaultMods.getKey(), id -> new ConcurrentHashMap<>());
|
||||||
|
for (var modLocale : defaultMap.modules.get(defaultMods.getKey()).entrySet()) {
|
||||||
|
module.putIfAbsent(modLocale.getKey(), modLocale.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void migrate() {
|
||||||
|
locale = new LocaleModel();
|
||||||
|
try {
|
||||||
|
var bf = new BufferedReader(new FileReader(path.toFile(), StandardCharsets.UTF_8));
|
||||||
|
var oldLocale = (Map<String, String>) gson.fromJson(bf, oldType);
|
||||||
|
|
||||||
|
for (var entry : oldLocale.entrySet()) {
|
||||||
|
var path = getPath(entry.getKey());
|
||||||
|
if (path == null) {
|
||||||
|
Constants.LOG.warn("Invalid locale path: {}", entry.getKey());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.type() == LocaleType.SHARED) {
|
||||||
|
locale.shared.put(path.key(), entry.getValue());
|
||||||
|
} else if (path.type() == LocaleType.MODULE) {
|
||||||
|
locale.modules
|
||||||
|
.computeIfAbsent(path.moduleId(), key -> new ConcurrentHashMap<>())
|
||||||
|
.put(path.key(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bf.close();
|
||||||
|
|
||||||
|
Constants.LOG.info("Successfully migrated locale!");
|
||||||
|
} catch (IOException | JsonSyntaxException e) {
|
||||||
|
Constants.LOG.error("Could not load locale", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> generateMap() {
|
||||||
|
var map = new HashMap<String, String>();
|
||||||
|
|
||||||
|
for (var entry : defaultMap.shared.entrySet()) {
|
||||||
|
map.put("shared." + entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var modEntry : defaultMap.modules.entrySet()) {
|
||||||
|
for (var entry : modEntry.getValue().entrySet()) {
|
||||||
|
map.put("module." + modEntry.getKey() + "." + entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reload() throws IOException {
|
||||||
|
load();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LocaleType {
|
||||||
|
SHARED,
|
||||||
|
MODULE
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class LocalePath {
|
||||||
|
private final LocaleType type;
|
||||||
|
private final String key;
|
||||||
|
private final @Nullable String moduleId;
|
||||||
|
|
||||||
|
public LocalePath(LocaleType type, String key, @Nullable String moduleId) {
|
||||||
|
this.type = type;
|
||||||
|
this.key = key;
|
||||||
|
this.moduleId = moduleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalePath(LocaleType type, String key) {
|
||||||
|
this(type, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocaleType type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String key() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String moduleId() {
|
||||||
|
return moduleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocaleModel {
|
||||||
|
public ConcurrentHashMap<String, String> shared = new ConcurrentHashMap<>();
|
||||||
|
public ConcurrentHashMap<String, ConcurrentHashMap<String, String>> modules = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public String get(String fullPath) {
|
||||||
|
var path = getPath(fullPath);
|
||||||
|
if (path == null) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.type() == LocaleType.SHARED) {
|
||||||
|
return shared.getOrDefault(path.key(), fullPath);
|
||||||
|
} else if (path.type() == LocaleType.MODULE) {
|
||||||
|
var module = modules.get(path.moduleId());
|
||||||
|
if (module == null) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
return module.getOrDefault(path.key(), fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package me.alexdevs.solstice.mixin;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.Constants;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(Minecraft.class)
|
||||||
|
public class MixinMinecraft {
|
||||||
|
|
||||||
|
@Inject(at = @At("TAIL"), method = "<init>")
|
||||||
|
private void init(CallbackInfo info) {
|
||||||
|
Constants.LOG.info("This line is printed by the Solstice common mixin!");
|
||||||
|
Constants.LOG.info("MC Version: {}", Minecraft.getInstance().getVersionType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package me.alexdevs.solstice.platform;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.Constants;
|
||||||
|
import me.alexdevs.solstice.platform.services.IEventHelper;
|
||||||
|
import me.alexdevs.solstice.platform.services.IPlatformHelper;
|
||||||
|
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
|
||||||
|
// Service loaders are a built-in Java feature that allow us to locate implementations of an interface that vary from one
|
||||||
|
// environment to another. In the context of MultiLoader we use this feature to access a mock API in the common code that
|
||||||
|
// is swapped out for the platform specific implementation at runtime.
|
||||||
|
public class Services {
|
||||||
|
|
||||||
|
// In this example we provide a platform helper which provides information about what platform the mod is running on.
|
||||||
|
// For example this can be used to check if the code is running on Forge vs Fabric, or to ask the modloader if another
|
||||||
|
// mod is loaded.
|
||||||
|
public static final IPlatformHelper PLATFORM = load(IPlatformHelper.class);
|
||||||
|
public static final IEventHelper EVENT = load(IEventHelper.class);
|
||||||
|
|
||||||
|
// This code is used to load a service for the current environment. Your implementation of the service must be defined
|
||||||
|
// manually by including a text file in META-INF/services named with the fully qualified class name of the service.
|
||||||
|
// Inside the file you should write the fully qualified class name of the implementation to load for the platform. For
|
||||||
|
// example our file on Forge points to ForgePlatformHelper while Fabric points to FabricPlatformHelper.
|
||||||
|
public static <T> T load(Class<T> clazz) {
|
||||||
|
final T loadedService = ServiceLoader.load(clazz).findFirst().orElseThrow(() -> new NullPointerException("Failed to load service for " + clazz.getName()));
|
||||||
|
Constants.LOG.debug("Loaded {} for service {}", loadedService, clazz);
|
||||||
|
return loadedService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package me.alexdevs.solstice.platform.event;
|
||||||
|
|
||||||
|
import com.google.common.collect.MapMaker;
|
||||||
|
|
||||||
|
import java.lang.reflect.Array;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class EventFactory {
|
||||||
|
private static final Set<Event<?>> EVENTS = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap());;
|
||||||
|
public static <T> Event<T> create(Class<? super T> type, Function<T[], T> invokerFactory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface AllowSleep {
|
||||||
|
boolean canSleep(String name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Event<T> {
|
||||||
|
private final Function<T[], T> invokerFactory;
|
||||||
|
private final Object lock = new Object();
|
||||||
|
private T[] handlers;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Event(Class<? super T> type, Function<T[], T> invokerFactory) {
|
||||||
|
this.invokerFactory = invokerFactory;
|
||||||
|
this.handlers = (T[]) Array.newInstance(type, 0);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
this.invoker = invokerFactory.apply(handlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package me.alexdevs.solstice.platform.services;
|
||||||
|
|
||||||
|
public interface IEventHelper {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package me.alexdevs.solstice.platform.services;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public interface IPlatformHelper {
|
||||||
|
|
||||||
|
enum Platform {
|
||||||
|
FABRIC("Fabric"),
|
||||||
|
NEOFORGE("NeoForge");
|
||||||
|
|
||||||
|
public final String prettyName;
|
||||||
|
|
||||||
|
Platform(String pretty) {
|
||||||
|
this.prettyName = pretty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the current platform
|
||||||
|
*
|
||||||
|
* @return The name of the current platform.
|
||||||
|
*/
|
||||||
|
Platform getPlatformName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a mod with the given id is loaded.
|
||||||
|
*
|
||||||
|
* @param modId The mod to check if it is loaded.
|
||||||
|
* @return True if the mod is loaded, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean isModLoaded(String modId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the game is currently in a development environment.
|
||||||
|
*
|
||||||
|
* @return True if in a development environment, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean isDevelopmentEnvironment();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the environment type as a string.
|
||||||
|
*
|
||||||
|
* @return The name of the environment type.
|
||||||
|
*/
|
||||||
|
default String getEnvironmentName() {
|
||||||
|
return isDevelopmentEnvironment() ? "development" : "production";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the version of Minecraft.
|
||||||
|
*
|
||||||
|
* @return The version of Minecraft.
|
||||||
|
*/
|
||||||
|
default String getMinecraftVersion() {
|
||||||
|
return Minecraft.getInstance().getVersionType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String getVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the version of the running loader.
|
||||||
|
* Uses Fabric API is Fabric.
|
||||||
|
*
|
||||||
|
* @return The version of the running loader.
|
||||||
|
*/
|
||||||
|
String getLoaderVersion();
|
||||||
|
|
||||||
|
Path getConfigDir();
|
||||||
|
|
||||||
|
Path getGameDir();
|
||||||
|
}
|
||||||
24
common/src/main/resources/info/formatting.txt
Normal file
24
common/src/main/resources/info/formatting.txt
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<gold>This page is an example of the formatting and placeholders used in pages and command texts.</gold>
|
||||||
|
|
||||||
|
<u>Formatting:</u>
|
||||||
|
|
||||||
|
Colors:
|
||||||
|
<yellow>yellow</yellow>, <dark_blue>dark_blue</dark_blue>, <dark_purple>dark_purple</dark_purple>, <gold>gold</gold>, <red>red</red>, <aqua>aqua</aqua>, <gray>gray</gray>, <light_purple>light_purple</light_purple>, <white>white</white>, <dark_gray>dark_gray</dark_gray>, <green>green</green>, <dark_green>dark_green</dark_green>, <blue>blue</blue>, <dark_aqua>dark_aqua</dark_aqua>, <dark_green>dark_green</dark_green>, <black>black</black>.
|
||||||
|
|
||||||
|
Decorations:
|
||||||
|
<strikethrough>strikethrough, st</strikethrough>, <underline>underline, underlined, u</underline>, <italic>italic, i</italic>, <obfuscated>obfuscated, obf</obfuscated>(obfuscated, obf), <bold>bold, b</bold>
|
||||||
|
|
||||||
|
Fonts:
|
||||||
|
<font:default>default</font>, <font:uniform>uniform</font>(uniform), <font:alt>alt</font>(alt)
|
||||||
|
|
||||||
|
Gradients:
|
||||||
|
<gradient:#ffffff:#000000>smooth white to black</gradient>, <hard_gradient:#ffffff:#000000>hard white to black</hard_gradient>, <rainbow>rainbow</rainbow>
|
||||||
|
|
||||||
|
<gold>For the complete documentation on text formatting check out <url:'https://placeholders.pb4.eu/user/text-format/'><hover:'Click to open link'><blue>this link</blue></hover></url>.</gold>
|
||||||
|
|
||||||
|
<u>Placeholders:</u>
|
||||||
|
|
||||||
|
Player name: %player:name%
|
||||||
|
Player display name: %player:displayname%
|
||||||
|
|
||||||
|
<gold>For the complete documentation on placeholders check out <url:'https://placeholders.pb4.eu/user/default-placeholders/'><hover:'Click to open link'><blue>this link</blue></hover></url>.</gold>
|
||||||
6
common/src/main/resources/info/motd.txt
Normal file
6
common/src/main/resources/info/motd.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<yellow><u>Welcome to the server, %player:displayname%!</u></yellow>
|
||||||
|
|
||||||
|
<gold>The world time is <yellow>%world:time%</yellow>.</gold>
|
||||||
|
<gold>There are <yellow>%server:online%</yellow>/<yellow>%server:max_players%</yellow> online players.</gold>
|
||||||
|
|
||||||
|
<gold>Make sure to read the <run_cmd:'/rules'><hover:'Read rules'><blue>/rules</blue></hover></run_cmd>!</gold>
|
||||||
3
common/src/main/resources/info/rules.txt
Normal file
3
common/src/main/resources/info/rules.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<gold>1.</gold> <yellow>Respect players.</yellow>
|
||||||
|
<gold>2.</gold> <yellow>Respect staff members.</yellow>
|
||||||
|
<gold>3.</gold> <yellow>Enjoy your stay!</yellow>
|
||||||
6
common/src/main/resources/pack.mcmeta
Normal file
6
common/src/main/resources/pack.mcmeta
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"pack": {
|
||||||
|
"description": "${mod_name}",
|
||||||
|
"pack_format": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
6
common/src/main/resources/solstice.accesswidener
Normal file
6
common/src/main/resources/solstice.accesswidener
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
accessWidener v2 named
|
||||||
|
# RTP
|
||||||
|
accessible method net/minecraft/server/level/ServerChunkCache getVisibleChunkIfPresent (J)Lnet/minecraft/server/level/ChunkHolder;
|
||||||
|
|
||||||
|
# Inventory see
|
||||||
|
accessible field net/minecraft/server/MinecraftServer playerDataStorage Lnet/minecraft/world/level/storage/PlayerDataStorage;
|
||||||
15
common/src/main/resources/solstice.mixins.json
Normal file
15
common/src/main/resources/solstice.mixins.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"minVersion": "0.8",
|
||||||
|
"package": "me.alexdevs.solstice.mixin",
|
||||||
|
"refmap": "${mod_id}.refmap.json",
|
||||||
|
"compatibilityLevel": "JAVA_18",
|
||||||
|
"mixins": [],
|
||||||
|
"client": [
|
||||||
|
"MixinMinecraft"
|
||||||
|
],
|
||||||
|
"server": [],
|
||||||
|
"injectors": {
|
||||||
|
"defaultRequire": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
50
fabric/build.gradle
Normal file
50
fabric/build.gradle
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
plugins {
|
||||||
|
id 'multiloader-loader'
|
||||||
|
id 'fabric-loom'
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
minecraft "com.mojang:minecraft:${minecraft_version}"
|
||||||
|
mappings loom.layered {
|
||||||
|
officialMojangMappings()
|
||||||
|
parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip")
|
||||||
|
}
|
||||||
|
modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}"
|
||||||
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}"
|
||||||
|
|
||||||
|
modImplementation include("org.spongepowered:configurate-core:${project.configurate_version}")
|
||||||
|
modImplementation include("org.spongepowered:configurate-hocon:${project.configurate_version}")
|
||||||
|
modImplementation include("org.spongepowered:configurate-gson:${project.configurate_version}")
|
||||||
|
include("com.typesafe:config:1.4.3")
|
||||||
|
include("io.leangen.geantyref:geantyref:1.3.16")
|
||||||
|
|
||||||
|
// Mod dependencies
|
||||||
|
|
||||||
|
include modImplementation("me.lucko:fabric-permissions-api:${project.permissions_api_version}")
|
||||||
|
|
||||||
|
include modImplementation("eu.pb4:placeholder-api:${project.placeholderapi_version}")
|
||||||
|
include modImplementation("eu.pb4:sgui:${project.sgui_version}")
|
||||||
|
|
||||||
|
modCompileOnly "dev.emi:trinkets:${project.trinkets_version}"
|
||||||
|
modRuntimeOnly "dev.emi:trinkets:${project.trinkets_version}"
|
||||||
|
|
||||||
|
modCompileOnly "net.luckperms:api:5.4"
|
||||||
|
modRuntimeOnly "net.luckperms:api:5.4"
|
||||||
|
}
|
||||||
|
|
||||||
|
loom {
|
||||||
|
def aw = project(':common').file("src/main/resources/${mod_id}.accesswidener")
|
||||||
|
if (aw.exists()) {
|
||||||
|
accessWidenerPath.set(aw)
|
||||||
|
}
|
||||||
|
mixin {
|
||||||
|
defaultRefmapName.set("${mod_id}.refmap.json")
|
||||||
|
}
|
||||||
|
runs {
|
||||||
|
server {
|
||||||
|
server()
|
||||||
|
setConfigName('Fabric Server')
|
||||||
|
ideConfigGenerated(true)
|
||||||
|
runDir('runs/server')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package me.alexdevs.solstice;
|
||||||
|
|
||||||
|
import net.fabricmc.api.ModInitializer;
|
||||||
|
|
||||||
|
public class SolsticeFabric implements ModInitializer {
|
||||||
|
@Override
|
||||||
|
public void onInitialize() {
|
||||||
|
Solstice.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package me.alexdevs.solstice.mixin;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.core.ToggleableConfig;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
|
||||||
|
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class SolsticeMixinConfigPlugin implements IMixinConfigPlugin {
|
||||||
|
private final ToggleableConfig config = ToggleableConfig.get();
|
||||||
|
public static final String packageBase = "me.alexdevs.solstice.mixin.modules.";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoad(String mixinPackage) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
|
||||||
|
if (mixinClassName.startsWith(packageBase)) {
|
||||||
|
var moduleMixin = mixinClassName.replace(packageBase, "");
|
||||||
|
var parts = moduleMixin.split("\\.");
|
||||||
|
var module = parts[0].toLowerCase();
|
||||||
|
return config.isEnabled(module);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRefMapperConfig() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getMixins() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package me.alexdevs.solstice.mixin.events;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.ParseResults;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import me.alexdevs.solstice.api.events.CommandEvents;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(CommandDispatcher.class)
|
||||||
|
public abstract class CommandEventsMixin<S> {
|
||||||
|
@Inject(method = "execute(Lcom/mojang/brigadier/ParseResults;)I", at = @At("HEAD"), remap = false)
|
||||||
|
public void execute(ParseResults<S> parse, CallbackInfoReturnable<Integer> cir) throws CommandSyntaxException {
|
||||||
|
var context = parse.getContext();
|
||||||
|
if (context.getSource() instanceof CommandSourceStack source) {
|
||||||
|
var command = parse.getReader().getString();
|
||||||
|
if (!CommandEvents.ALLOW_COMMAND.invoker().allowCommand(source, command)) {
|
||||||
|
cir.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandEvents.COMMAND.invoker().onCommand(source, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package me.alexdevs.solstice.mixin.events;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.api.events.WorldSaveCallback;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(MinecraftServer.class)
|
||||||
|
public class WorldSaveEventMixin {
|
||||||
|
@Inject(method = "saveEverything", at = @At("TAIL"))
|
||||||
|
public void save(boolean suppressLogs, boolean flush, boolean force, CallbackInfoReturnable<Boolean> cir) {
|
||||||
|
try {
|
||||||
|
WorldSaveCallback.EVENT.invoker().onSave((MinecraftServer) (Object) this, suppressLogs, flush, force);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SolsticeFabric.LOGGER.error("Exception emitting world save event", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.admin;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.api.events.PlayerConnectionEvents;
|
||||||
|
import net.minecraft.server.dedicated.DedicatedPlayerList;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(DedicatedPlayerList.class)
|
||||||
|
public abstract class ConnectionBypassMixin {
|
||||||
|
@Inject(method = "isWhiteListed", at = @At("HEAD"), cancellable = true)
|
||||||
|
public void solstice$bypassWhitelist(GameProfile profile, CallbackInfoReturnable<Boolean> cir) {
|
||||||
|
try {
|
||||||
|
if (PlayerConnectionEvents.WHITELIST_BYPASS.invoker().bypassWhitelist(profile))
|
||||||
|
cir.setReturnValue(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SolsticeFabric.LOGGER.error("Error checking whitelist bypass for profile {}", profile.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "canBypassPlayerLimit", at = @At("HEAD"), cancellable = true)
|
||||||
|
public void solstice$bypassPlayerLimit(GameProfile profile, CallbackInfoReturnable<Boolean> cir) {
|
||||||
|
try {
|
||||||
|
if (PlayerConnectionEvents.FULL_SERVER_BYPASS.invoker().bypassFullServer(profile))
|
||||||
|
cir.setReturnValue(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SolsticeFabric.LOGGER.error("Error checking full server bypass for profile {}", profile.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.afk;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.modules.afk.AfkModule;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.server.players.SleepStatus;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
|
||||||
|
@Mixin(SleepStatus.class)
|
||||||
|
public abstract class FixPlayerSleepPercentageMixin {
|
||||||
|
@Redirect(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;isSpectator()Z"))
|
||||||
|
public boolean solstice$fixTotalPlayers(ServerPlayer player) {
|
||||||
|
var afkModule = SolsticeFabric.modules.getModule(AfkModule.class);
|
||||||
|
|
||||||
|
return player.isSpectator() || afkModule.isPlayerAfk(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.back;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.api.ServerLocation;
|
||||||
|
import me.alexdevs.solstice.modules.back.BackModule;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.RelativeMovement;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Mixin(ServerPlayer.class)
|
||||||
|
public abstract class PreTeleportMixin {
|
||||||
|
@Inject(method = "teleportTo(Lnet/minecraft/server/level/ServerLevel;DDDLjava/util/Set;FF)Z", at = @At("HEAD"))
|
||||||
|
public void solstice$getPreTeleportLocation(ServerLevel world, double destX, double destY, double destZ, Set<RelativeMovement> flags, float yaw, float pitch, CallbackInfoReturnable<Boolean> cir) {
|
||||||
|
var player = (ServerPlayer) (Object) this;
|
||||||
|
SolsticeFabric.modules.getModule(BackModule.class).lastPlayerPositions.put(player.getUUID(), new ServerLocation(player));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.ban;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.sugar.Local;
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.modules.ban.formatters.BanMessageFormatter;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.MutableComponent;
|
||||||
|
import net.minecraft.server.players.PlayerList;
|
||||||
|
import net.minecraft.server.players.UserBanListEntry;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
|
||||||
|
@Mixin(PlayerList.class)
|
||||||
|
public abstract class CustomBanMessageMixin {
|
||||||
|
@Inject(method = "canPlayerLogin", at = @At(value = "RETURN", ordinal = 0), cancellable = true)
|
||||||
|
public void solstice$formatBanMessage(SocketAddress address, GameProfile profile, CallbackInfoReturnable<Component> cir, @Local UserBanListEntry bannedPlayerEntry, @Local MutableComponent mutableText) {
|
||||||
|
try {
|
||||||
|
var reasonText = BanMessageFormatter.format(profile, bannedPlayerEntry);
|
||||||
|
cir.setReturnValue(reasonText);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
SolsticeFabric.LOGGER.error("Something went wrong while formatting the ban message", ex);
|
||||||
|
|
||||||
|
// Ensure the original text message is returned to avoid exploits and bypass the ban
|
||||||
|
cir.setReturnValue(mutableText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.core;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.sugar.Local;
|
||||||
|
import net.minecraft.server.network.ServerCommonPacketListenerImpl;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
|
||||||
|
@Mixin(ServerCommonPacketListenerImpl.class)
|
||||||
|
public abstract class RealPingMixin {
|
||||||
|
@Shadow
|
||||||
|
private int latency;
|
||||||
|
|
||||||
|
@Redirect(method = "handleKeepAlive", at = @At(value = "FIELD", target = "Lnet/minecraft/server/network/ServerCommonPacketListenerImpl;latency:I", opcode = Opcodes.PUTFIELD))
|
||||||
|
public void solstice$realPing(ServerCommonPacketListenerImpl instance, int value, @Local int i) {
|
||||||
|
latency = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.customname;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.modules.customName.CustomNameModule;
|
||||||
|
import net.minecraft.network.chat.MutableComponent;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(Player.class)
|
||||||
|
public abstract class CustomDisplayNameMixin {
|
||||||
|
@Shadow
|
||||||
|
private MutableComponent decorateDisplayNameComponent(MutableComponent component) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "getDisplayName", at = @At("HEAD"), cancellable = true)
|
||||||
|
public void solstice$getDisplayName(CallbackInfoReturnable<MutableComponent> cir) {
|
||||||
|
var customNameModule = SolsticeFabric.modules.getModule(CustomNameModule.class);
|
||||||
|
var name = customNameModule.getNameForPlayer((ServerPlayer) (Object) this);
|
||||||
|
cir.setReturnValue(decorateDisplayNameComponent(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.miscellaneous;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.modules.miscellaneous.MiscellaneousModule;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(LivingEntity.class)
|
||||||
|
public abstract class BypassSleepingInBedCheckMixin {
|
||||||
|
@Inject(method = "checkBedExists", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void isSleepingInBed(CallbackInfoReturnable<Boolean> cir) {
|
||||||
|
var module = SolsticeFabric.modules.getModule(MiscellaneousModule.class);
|
||||||
|
if (module.isCommandSleep((LivingEntity) (Object) this)) {
|
||||||
|
cir.setReturnValue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.sign;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.modules.sign.SignModule;
|
||||||
|
import net.minecraft.server.network.FilteredText;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.level.block.entity.SignBlockEntity;
|
||||||
|
import net.minecraft.world.level.block.entity.SignText;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mixin(SignBlockEntity.class)
|
||||||
|
public abstract class FormatSignMixin {
|
||||||
|
|
||||||
|
@Inject(method = "setMessages", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void solstice$formatSignText(Player player, List<FilteredText> messages, SignText text, CallbackInfoReturnable<SignText> cir) {
|
||||||
|
var formattableSignsModule = SolsticeFabric.modules.getModule(SignModule.class);
|
||||||
|
if (formattableSignsModule.canFormatSign(player)) {
|
||||||
|
try {
|
||||||
|
text = SignModule.formatSign(messages, text);
|
||||||
|
cir.setReturnValue(text);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SolsticeFabric.LOGGER.error("Something went wrong while formatting a sign!", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.spawn;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.modules.spawn.SpawnModule;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.portal.DimensionTransition;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.spongepowered.asm.mixin.Final;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(ServerPlayer.class)
|
||||||
|
public abstract class OverrideSpawnPointMixin {
|
||||||
|
@Shadow
|
||||||
|
@Final
|
||||||
|
public MinecraftServer server;
|
||||||
|
|
||||||
|
@Inject(method = "getRespawnPosition", at = @At("RETURN"), cancellable = true)
|
||||||
|
public void solstice$overrideSpawnPos(CallbackInfoReturnable<BlockPos> cir) {
|
||||||
|
var spawnModule = SolsticeFabric.modules.getModule(SpawnModule.class);
|
||||||
|
var config = spawnModule.getConfig();
|
||||||
|
if (config.globalSpawn.onRespawn) {
|
||||||
|
var pos = spawnModule.getGlobalSpawnPosition().getBlockPos();
|
||||||
|
cir.setReturnValue(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "getRespawnDimension", at = @At("RETURN"), cancellable = true)
|
||||||
|
public void solstice$overrideSpawnDimension(CallbackInfoReturnable<ResourceKey<Level>> cir) {
|
||||||
|
var spawnModule = SolsticeFabric.modules.getModule(SpawnModule.class);
|
||||||
|
var config = spawnModule.getConfig();
|
||||||
|
if (config.globalSpawn.onRespawn) {
|
||||||
|
cir.setReturnValue(spawnModule.getGlobalSpawnWorld().dimension());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "findRespawnPositionAndUseSpawnBlock", at = @At("RETURN"), cancellable = true)
|
||||||
|
public void solstice$overrideRespawnTarget(boolean keepInventory, DimensionTransition.PostDimensionTransition postDimensionTransition, CallbackInfoReturnable<DimensionTransition> cir) {
|
||||||
|
var spawnModule = SolsticeFabric.modules.getModule(SpawnModule.class);
|
||||||
|
var config = spawnModule.getConfig();
|
||||||
|
if (config.globalSpawn.onRespawn) {
|
||||||
|
var spawn = spawnModule.getGlobalSpawnPosition();
|
||||||
|
|
||||||
|
var world = spawn.getWorld(this.server);
|
||||||
|
var pos = new Vec3(
|
||||||
|
spawn.getX(),
|
||||||
|
spawn.getY(),
|
||||||
|
spawn.getZ()
|
||||||
|
);
|
||||||
|
|
||||||
|
cir.setReturnValue(new DimensionTransition(
|
||||||
|
world,
|
||||||
|
pos,
|
||||||
|
Vec3.ZERO,
|
||||||
|
spawn.getYaw(),
|
||||||
|
spawn.getPitch(),
|
||||||
|
false,
|
||||||
|
DimensionTransition.DO_NOTHING
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.styling;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.modules.styling.formatters.AdvancementFormatter;
|
||||||
|
import net.minecraft.advancements.AdvancementHolder;
|
||||||
|
import net.minecraft.advancements.AdvancementType;
|
||||||
|
import net.minecraft.network.chat.MutableComponent;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(AdvancementType.class)
|
||||||
|
public abstract class CustomAdvancementMixin {
|
||||||
|
|
||||||
|
@Inject(method = "createAnnouncement", at = @At("HEAD"), cancellable = true)
|
||||||
|
public void solstice$getCustomAnnouncement(AdvancementHolder advancement, ServerPlayer player, CallbackInfoReturnable<MutableComponent> cir) {
|
||||||
|
cir.setReturnValue(AdvancementFormatter.getText(player, advancement, (AdvancementType) (Object) this).copy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.styling;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.sugar.Local;
|
||||||
|
import me.alexdevs.solstice.modules.styling.CustomSentMessage;
|
||||||
|
import net.minecraft.network.chat.OutgoingChatMessage;
|
||||||
|
import net.minecraft.network.chat.PlayerChatMessage;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.server.players.PlayerList;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
|
||||||
|
@Mixin(PlayerList.class)
|
||||||
|
public abstract class CustomChatMessageMixin {
|
||||||
|
@Redirect(
|
||||||
|
method = "broadcastChatMessage(Lnet/minecraft/network/chat/PlayerChatMessage;Ljava/util/function/Predicate;Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/network/chat/ChatType$Bound;)V",
|
||||||
|
at = @At(
|
||||||
|
value = "INVOKE",
|
||||||
|
target = "Lnet/minecraft/network/chat/OutgoingChatMessage;create(Lnet/minecraft/network/chat/PlayerChatMessage;)Lnet/minecraft/network/chat/OutgoingChatMessage;")
|
||||||
|
)
|
||||||
|
private OutgoingChatMessage solstice$broadcast(PlayerChatMessage message, @Local(argsOnly = true) ServerPlayer sender) {
|
||||||
|
return CustomSentMessage.of(message, sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.styling;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.modules.styling.formatters.ConnectionActivityFormatter;
|
||||||
|
import net.minecraft.network.Connection;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.server.network.CommonListenerCookie;
|
||||||
|
import net.minecraft.server.players.PlayerList;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Unique;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(PlayerList.class)
|
||||||
|
public abstract class CustomConnectionMessagesMixin {
|
||||||
|
@Unique
|
||||||
|
private ServerPlayer solstice$player = null;
|
||||||
|
|
||||||
|
@Inject(method = "placeNewPlayer", at = @At("HEAD"))
|
||||||
|
private void solstice$onJoin(Connection connection, ServerPlayer player, CommonListenerCookie cookie, CallbackInfo ci) {
|
||||||
|
solstice$player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "placeNewPlayer", at = @At("RETURN"))
|
||||||
|
private void solstice$onJoinReturn(Connection connection, ServerPlayer player, CommonListenerCookie cookie, CallbackInfo ci) {
|
||||||
|
solstice$player = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ModifyArg(method = "placeNewPlayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/players/PlayerList;broadcastSystemMessage(Lnet/minecraft/network/chat/Component;Z)V"))
|
||||||
|
public Component solstice$getPlayerJoinMessage(Component message) {
|
||||||
|
var ogText = (TranslatableContents) message.getContents();
|
||||||
|
var args = ogText.getArgs();
|
||||||
|
|
||||||
|
if (args.length == 1) {
|
||||||
|
return ConnectionActivityFormatter.onJoin(solstice$player);
|
||||||
|
} else {
|
||||||
|
return ConnectionActivityFormatter.onJoinRenamed(solstice$player, (String) args[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.styling;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.modules.styling.formatters.DeathFormatter;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.damagesource.CombatTracker;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
|
||||||
|
@Mixin(ServerPlayer.class)
|
||||||
|
public abstract class CustomDeathMessageMixin {
|
||||||
|
@Redirect(method = "die", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/damagesource/CombatTracker;getDeathMessage()Lnet/minecraft/network/chat/Component;"))
|
||||||
|
private Component solstice$getDeathMessage(CombatTracker instance) {
|
||||||
|
var player = (ServerPlayer) (Object) this;
|
||||||
|
return DeathFormatter.onDeath(player, instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.styling;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.SolsticeFabric;
|
||||||
|
import me.alexdevs.solstice.modules.styling.CustomSentMessage;
|
||||||
|
import net.minecraft.network.chat.OutgoingChatMessage;
|
||||||
|
import net.minecraft.network.chat.PlayerChatMessage;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(OutgoingChatMessage.class)
|
||||||
|
public interface CustomSentMessageMixin {
|
||||||
|
@Inject(method = "create", at = @At("HEAD"), cancellable = true)
|
||||||
|
private static void solstice$of(PlayerChatMessage message, CallbackInfoReturnable<OutgoingChatMessage> cir) {
|
||||||
|
if (message.isSystem()) {
|
||||||
|
cir.setReturnValue(new CustomSentMessage.Profileless(message.decoratedContent()));
|
||||||
|
} else {
|
||||||
|
var sender = SolsticeFabric.server.getPlayerList().getPlayer(message.sender());
|
||||||
|
cir.setReturnValue(new CustomSentMessage.Chat(message, sender));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.styling;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import me.alexdevs.solstice.modules.styling.StylingModule;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.resources.RegistryDataLoader;
|
||||||
|
import net.minecraft.resources.RegistryOps;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Coerce;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Mixin(RegistryDataLoader.class)
|
||||||
|
public class InjectCustomChatMessageMixin {
|
||||||
|
|
||||||
|
/*
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Inject(method = "load(Lnet/minecraft/resource/ResourceManager;Lnet/minecraft/registry/DynamicRegistryManager;Ljava/util/List;)Lnet/minecraft/registry/DynamicRegistryManager$Immutable;", at = @At(value = "INVOKE", target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V", ordinal = 0, shift = At.Shift.AFTER), locals = LocalCapture.CAPTURE_FAILEXCEPTION)
|
||||||
|
private static void solstice$load(ResourceManager resourceManager, DynamicRegistryManager baseRegistryManager, List<RegistryLoader.Entry<?>> entries,
|
||||||
|
CallbackInfoReturnable<DynamicRegistryManager.Immutable> cir, Map _unused, List<Pair<MutableRegistry<?>, Object>> list) {
|
||||||
|
for (var pair : list) {
|
||||||
|
var registry = pair.getFirst();
|
||||||
|
if (registry.getKey().equals(RegistryKeys.MESSAGE_TYPE)) {
|
||||||
|
Registry.register((Registry<MessageType>) registry, StylingModule.CHAT_TYPE,
|
||||||
|
new MessageType(
|
||||||
|
Decoration.ofChat("%s"),
|
||||||
|
Decoration.ofChat("%s")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*@SuppressWarnings("unchecked")
|
||||||
|
@Inject(method = "Lnet/minecraft/resources/RegistryDataLoader;load(Lnet/minecraft/resources/RegistryDataLoader$LoadingFunction;Lnet/minecraft/core/RegistryAccess;Ljava/util/List;)Lnet/minecraft/core/RegistryAccess$Frozen;",
|
||||||
|
at = @At(value = "INVOKE", target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V",
|
||||||
|
ordinal = 0, shift = At.Shift.AFTER), locals = LocalCapture.CAPTURE_FAILEXCEPTION)
|
||||||
|
private static void solstice$load(RegistryDataLoader.LoadingFunction loadingFunction, RegistryAccess registryAccess, List<RegistryDataLoader.RegistryData<?>> registryData, CallbackInfoReturnable<RegistryAccess.Frozen> cir, Map map, List list, RegistryOps.RegistryInfoLookup registryInfoLookup) {
|
||||||
|
for (var entry : entries) {
|
||||||
|
var registry = entry.key();
|
||||||
|
if (registry.getRegistry().equals(RegistryKeys.MESSAGE_TYPE)) {
|
||||||
|
Registry.register((Registry<MessageType>) registry, StylingModule.CHAT_TYPE,
|
||||||
|
new MessageType(
|
||||||
|
Decoration.ofChat("%s"),
|
||||||
|
Decoration.ofChat("%s")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package me.alexdevs.solstice.mixin.modules.styling;
|
||||||
|
|
||||||
|
import me.alexdevs.solstice.modules.styling.formatters.ConnectionActivityFormatter;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||||
|
|
||||||
|
@Mixin(ServerGamePacketListenerImpl.class)
|
||||||
|
public abstract class PlayerDisconnectMixin {
|
||||||
|
@Shadow
|
||||||
|
public ServerPlayer player;
|
||||||
|
|
||||||
|
@ModifyArg(method = "removePlayerFromWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/players/PlayerList;broadcastSystemMessage(Lnet/minecraft/network/chat/Component;Z)V"))
|
||||||
|
private Component solstice$getPlayerLeaveMessage(Component message) {
|
||||||
|
return ConnectionActivityFormatter.onLeave(this.player);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue