Initial commit

This commit is contained in:
Alessandro Proto 2025-03-10 23:20:06 +01:00
commit d3838f6a78
119 changed files with 6112 additions and 0 deletions

119
.gitignore vendored Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
plugins {
id 'groovy-gradle-plugin'
}

View 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')
}
}
}

View 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
View 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
}

View 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);
}

View 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);
}
}

View file

@ -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();
}
}

View 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;
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}
}
}
}
}
}
}

View file

@ -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);
}
}

View file

@ -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) {
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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)
);
}
}

View file

@ -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());
}
}

View file

@ -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)));
};
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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);
}
}
}
}

View file

@ -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);
}
}

View file

@ -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) {
}
}

View 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;
}
}
}

View file

@ -0,0 +1,7 @@
package me.alexdevs.solstice.core;
public class WarmUpManager {
public WarmUpManager() {
}
}

View file

@ -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();
}
}

View file

@ -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;
})
);
}
}

View file

@ -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;
});
}
}

View file

@ -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;
}))
);
}
}

View file

@ -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}";
}

View file

@ -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>")
);
}

View file

@ -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;
}

View 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);
}
}

View file

@ -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();
}
}
}

View 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);
}
}

View file

@ -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);
}
}
}

View file

@ -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");
}
}

View 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);
}
}

View file

@ -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;
}
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,4 @@
package me.alexdevs.solstice.platform.services;
public interface IEventHelper {
}

View file

@ -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();
}

View 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>

View 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>

View 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>

View file

@ -0,0 +1,6 @@
{
"pack": {
"description": "${mod_name}",
"pack_format": 8
}
}

View 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;

View 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
View 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')
}
}
}

View file

@ -0,0 +1,10 @@
package me.alexdevs.solstice;
import net.fabricmc.api.ModInitializer;
public class SolsticeFabric implements ModInitializer {
@Override
public void onInitialize() {
Solstice.init();
}
}

View file

@ -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) {
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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
));
}
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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]);
}
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}
}

View file

@ -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")
));
}
}
}*/
}

View file

@ -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