1
0
mirror of https://github.com/chylex/Minecraft-Window-Title.git synced 2025-05-05 04:34:06 +02:00

Implement custom window title w/ {mcversion} and {modversion} tokens

This commit is contained in:
chylex 2020-01-31 13:06:23 +01:00
parent abaa23c5c2
commit c6102427aa
14 changed files with 384 additions and 0 deletions

View File

@ -0,0 +1,46 @@
package chylex.customwindowtitle;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class TitleParser{
private static final Pattern tokenRegex = Pattern.compile("\\{([a-z]+)(?::([^}]+))?}");
private static final Logger logger = LogManager.getLogger("CustomWindowTitle");
public static String parse(String input){
StringBuffer buffer = new StringBuffer();
Matcher matcher = tokenRegex.matcher(input);
while(matcher.find()){
String token = matcher.group(1);
String[] args = StringUtils.split(matcher.group(2), ',');
String result = null;
try{
result = TitleTokens.getTokenFunction(token).apply(args == null ? ArrayUtils.EMPTY_STRING_ARRAY : args);
}catch(TokenException e){
logger.warn("Error processing token '" + token + "': " + e.getMessage());
}catch(Throwable t){
logger.warn("Error processing token '" + token + "': " + t.getMessage(), t);
}
if (result == null){
matcher.appendReplacement(buffer, input.substring(matcher.start(), matcher.end()));
}
else{
matcher.appendReplacement(buffer, result);
}
}
matcher.appendTail(buffer);
return buffer.toString();
}
// Static class
private TitleParser(){}
}

View File

@ -0,0 +1,43 @@
package chylex.customwindowtitle;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
public final class TitleTokens{
// Registry
private static final Map<String, Function<String[], String>> tokenMap = new HashMap<>();
public static void registerToken(String token, Function<String[], String> processor){
tokenMap.putIfAbsent(token, processor);
}
public static Function<String[], String> getTokenFunction(String token){
return tokenMap.getOrDefault(token, args -> null);
}
// Arguments
public static Function<String[], String> noArgs(Supplier<String> func){
return args -> args.length > 0 ? fail("expected no arguments, got " + args.length) : func.get();
}
public static Function<String[], String> oneArg(UnaryOperator<String> func){
return args -> args.length != 1 ? fail("expected 1 argument, got " + args.length) : func.apply(args[0]);
}
public static Function<String[], String> rangeArgs(int min, int max, Function<String[], String> func){
return args -> args.length < min || args.length > max ? fail("expected between " + min + " and " + max + " arguments, got " + args.length) : func.apply(args);
}
private static String fail(String message){
throw new TokenException(message);
}
// Static class
private TitleTokens(){}
}

View File

@ -0,0 +1,7 @@
package chylex.customwindowtitle;
public class TokenException extends RuntimeException{
public TokenException(String message){
super(message);
}
}

View File

@ -0,0 +1,49 @@
package chylex.customwindowtitle.fabric;
import chylex.customwindowtitle.TitleParser;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.MinecraftClient;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
public class CustomWindowTitle implements ClientModInitializer{
private static final String defaultTitle = "Minecraft {mcversion}";
private String configTitle;
@Override
public void onInitializeClient(){
Path configFile = Paths.get(FabricLoader.getInstance().getConfigDirectory().getAbsolutePath(), "customwindowtitle-client.toml");
try{
String prefix = "title = ";
if (!Files.exists(configFile)){
Files.write(configFile, Collections.singletonList(prefix + '"' + defaultTitle + '"'), StandardCharsets.UTF_8);
configTitle = defaultTitle;
}
else{
configTitle = Files
.readAllLines(configFile, StandardCharsets.UTF_8)
.stream()
.filter(line -> line.startsWith(prefix))
.map(line -> StringUtils.strip(StringUtils.removeStart(line, prefix).trim(), "\""))
.findFirst()
.orElse(defaultTitle);
}
}catch(IOException e){
throw new RuntimeException("CustomWindowTitle configuration error", e);
}
TokenData.register();
MinecraftClient.getInstance().execute(this::updateTitle);
}
private void updateTitle(){
MinecraftClient.getInstance().getWindow().setTitle(TitleParser.parse(configTitle));
}
}

View File

@ -0,0 +1,22 @@
package chylex.customwindowtitle.fabric;
import chylex.customwindowtitle.TokenException;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.SharedConstants;
import static chylex.customwindowtitle.TitleTokens.noArgs;
import static chylex.customwindowtitle.TitleTokens.oneArg;
import static chylex.customwindowtitle.TitleTokens.registerToken;
final class TokenData{
static void register(){
registerToken("mcversion", noArgs(TokenData::getMinecraftVersion));
registerToken("modversion", oneArg(TokenData::getModVersion));
}
static String getMinecraftVersion(){
return SharedConstants.getGameVersion().getName();
}
static String getModVersion(String modId){
return FabricLoader.getInstance().getModContainer(modId).orElseThrow(() -> new TokenException("mod info for '" + modId + "' not found")).getMetadata().getVersion().getFriendlyString();
}
}

View File

@ -0,0 +1,14 @@
package chylex.customwindowtitle.fabric.mixin;
import net.minecraft.client.MinecraftClient;
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(MinecraftClient.class)
public final class DisableVanillaTitle{
@Inject(method = "updateWindowTitle()V", at = @At("HEAD"), cancellable = true)
private void updateTitle(CallbackInfo info){
info.cancel();
}
}

View File

@ -0,0 +1,13 @@
{
"required": true,
"package": "chylex.customwindowtitle.fabric.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": [
],
"client": [
"DisableVanillaTitle"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@ -0,0 +1,46 @@
package chylex.customwindowtitle;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class TitleParser{
private static final Pattern tokenRegex = Pattern.compile("\\{([a-z]+)(?::([^}]+))?}");
private static final Logger logger = LogManager.getLogger("CustomWindowTitle");
public static String parse(String input){
StringBuffer buffer = new StringBuffer();
Matcher matcher = tokenRegex.matcher(input);
while(matcher.find()){
String token = matcher.group(1);
String[] args = StringUtils.split(matcher.group(2), ',');
String result = null;
try{
result = TitleTokens.getTokenFunction(token).apply(args == null ? ArrayUtils.EMPTY_STRING_ARRAY : args);
}catch(TokenException e){
logger.warn("Error processing token '" + token + "': " + e.getMessage());
}catch(Throwable t){
logger.warn("Error processing token '" + token + "': " + t.getMessage(), t);
}
if (result == null){
matcher.appendReplacement(buffer, input.substring(matcher.start(), matcher.end()));
}
else{
matcher.appendReplacement(buffer, result);
}
}
matcher.appendTail(buffer);
return buffer.toString();
}
// Static class
private TitleParser(){}
}

View File

@ -0,0 +1,43 @@
package chylex.customwindowtitle;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
public final class TitleTokens{
// Registry
private static final Map<String, Function<String[], String>> tokenMap = new HashMap<>();
public static void registerToken(String token, Function<String[], String> processor){
tokenMap.putIfAbsent(token, processor);
}
public static Function<String[], String> getTokenFunction(String token){
return tokenMap.getOrDefault(token, args -> null);
}
// Arguments
public static Function<String[], String> noArgs(Supplier<String> func){
return args -> args.length > 0 ? fail("expected no arguments, got " + args.length) : func.get();
}
public static Function<String[], String> oneArg(UnaryOperator<String> func){
return args -> args.length != 1 ? fail("expected 1 argument, got " + args.length) : func.apply(args[0]);
}
public static Function<String[], String> rangeArgs(int min, int max, Function<String[], String> func){
return args -> args.length < min || args.length > max ? fail("expected between " + min + " and " + max + " arguments, got " + args.length) : func.apply(args);
}
private static String fail(String message){
throw new TokenException(message);
}
// Static class
private TitleTokens(){}
}

View File

@ -0,0 +1,7 @@
package chylex.customwindowtitle;
public class TokenException extends RuntimeException{
public TokenException(String message){
super(message);
}
}

View File

@ -0,0 +1,36 @@
package chylex.customwindowtitle.forge;
import chylex.customwindowtitle.TitleParser;
import net.minecraft.client.Minecraft;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.common.ForgeConfigSpec.ConfigValue;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig.Type;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
@Mod("customwindowtitle")
public class CustomWindowTitle{
private final ConfigValue<String> configTitle;
public CustomWindowTitle(){
ForgeConfigSpec.Builder configBuilder = new ForgeConfigSpec.Builder();
configTitle = configBuilder.define("title", "Minecraft {mcversion}");
ModLoadingContext.get().registerConfig(Type.CLIENT, configBuilder.build());
FMLJavaModLoadingContext.get().getModEventBus().register(this);
TokenData.register();
}
@SubscribeEvent
public void onClientSetup(FMLClientSetupEvent e){
e.getMinecraftSupplier().get().execute(this::updateTitle);
}
private void updateTitle(){
Minecraft.getInstance().getMainWindow().func_230148_b_(TitleParser.parse(configTitle.get()));
}
}

View File

@ -0,0 +1,36 @@
package chylex.customwindowtitle.forge;
import chylex.customwindowtitle.TokenException;
import net.minecraft.util.SharedConstants;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
import net.minecraftforge.forgespi.language.IModInfo;
import static chylex.customwindowtitle.TitleTokens.noArgs;
import static chylex.customwindowtitle.TitleTokens.oneArg;
import static chylex.customwindowtitle.TitleTokens.registerToken;
final class TokenData{
static void register(){
registerToken("mcversion", noArgs(TokenData::getMinecraftVersion));
registerToken("modversion", oneArg(TokenData::getModVersion));
}
static String getMinecraftVersion(){
return SharedConstants.getVersion().getName();
}
static String getModVersion(String modId){
ModFileInfo file = ModList.get().getModFileById(modId);
if (file == null){
throw new TokenException("mod file for '" + modId + "' not found");
}
for(IModInfo info : file.getMods()){
if (info.getModId().equals(modId)){
return info.getVersion().toString();
}
}
throw new TokenException("mod info for '" + modId + "' not found");
}
}

View File

@ -0,0 +1,3 @@
{
"CustomWindowTitle": "coremods/main.js"
}

View File

@ -0,0 +1,19 @@
function initializeCoreMod(){
var opcodes = Java.type("org.objectweb.asm.Opcodes");
var InsnNode = Java.type("org.objectweb.asm.tree.InsnNode");
return {
"CustomWindowTitle": {
"target": {
"type": "METHOD",
"class": "net.minecraft.client.Minecraft",
"methodName": "func_230150_b_",
"methodDesc": "()V"
},
"transformer": function(methodNode){
methodNode.instructions.insert(new InsnNode(opcodes.RETURN));
return methodNode;
}
}
};
}