Localization failed to switch while enable BepInEx mod

Mod issue Localization
Neutron3529 [github] · 3 years ago

Issue description

I tried compile BepInEx Mod Plugin, it loaded normally, modify the electric cost and output count to make the game easier.

But when the mod is loaded, the i18n content just disappear. I could only found UI in English.

Is it a BUG or it is designed as a part of anti-cheat procedure?

Expected behavior

The game loaded normally.

Reproduction steps

Here is my mod, but the source is also provided in case you want to compile the mod from source.
Mod.zip

Captain of Industry.cs (actually it is a shell file.)

#!/bin/bash -e
#
#   Neutron3529's Unity Game Plugin
#   Copyright (C) 2022-2023 Neutron3529
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Affero General Public License as
#   published by the Free Software Foundation, either version 3 of the
#   License, or (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Affero General Public License for more details.
#
#   You should have received a copy of the GNU Affero General Public License
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
############################################################################
#
#   * compile instructions: put this file and `utils.cs` in `steamapps`
#   * folder, open a terminal in the same folder, and execute:
#   *
#   * ```
#   *     chmod +x ${file}.cs
#   *     ./${file}.cs
#   * ```
#   *
#   * then the mod will be compiled automatically.
#   *
#   * Here we wrote a shebang like file, which is correct
#   * in my computer (Manjaro XFCE), if such script do not work
#   * in your computer, you could just try the instructions below :

export GAME_NAME="${0%\.cs}"                                # might modify if the name mismatch.
export GAME_DIR="$GAME_NAME"                                # might be modified, but "$GAME_NAME" cover most of the cases.

export FILE_NAME="$0"
export ASSEMBLY="Mafi"                           # might be modified
export UTILS="utils.cs"                                     # might be modified if you do not put utils.cs in the current dir.
export PLUGIN_ID="Neutron3529.Cheat"                        # should be modified
export NAMESPACE_ID="Neutron3529.Cheat"                     # should be modified
export GAME_BASE_DIR="common/$GAME_DIR"                     # should modify GAME_DIR instead since GAME_DIR == GAME_NAME is almost always true.

export IFS=$'\n' # to disable the annoying space.
export DOTNET="dotnet" # the location of the DOTNET executable file.
[ -z "$DOTNET_CSC_DLL" ] && export DOTNET_CSC_DLL=`\ls /usr/share/dotnet/sdk/*/Roslyn/bincore/csc.dll` # In manjaro, the csc.dll is located in /usr/share/dotnet/sdk/*/Roslyn/bincore/csc.dll

case $1 in
    V)       EXTRA_DEFINE="-define:DEBUG${IFS}-define:VERBOSE${IFS}-debug" ;;
    v)       EXTRA_DEFINE="-define:DEBUG${IFS}-define:VERBOSE${IFS}-debug" ;;
    VERBOSE) EXTRA_DEFINE="-define:DEBUG${IFS}-define:VERBOSE${IFS}-debug" ;;
    verbose) EXTRA_DEFINE="-define:DEBUG${IFS}-define:VERBOSE${IFS}-debug" ;;
    D)       EXTRA_DEFINE="-define:DEBUG${IFS}-debug"                      ;;
    d)       EXTRA_DEFINE="-define:DEBUG${IFS}-debug"                      ;;
    DEBUG)   EXTRA_DEFINE="-define:DEBUG${IFS}-debug"                      ;;
    debug)   EXTRA_DEFINE="-define:DEBUG${IFS}-debug"                      ;;
    *)       EXTRA_DEFINE=""                                               ;;
esac

_MODE__SELECT_=100

( yes "" | head -n $_MODE__SELECT_ | head -n-1  ; tail $FILE_NAME -n+$_MODE__SELECT_) | sed s/%%NAMESPACE_ID%%/${NAMESPACE_ID}/g | sed s/%%PLUGIN_ID%%/${PLUGIN_ID}/g | $DOTNET $DOTNET_CSC_DLL -nologo -t:library \
  -r:"${GAME_BASE_DIR}/BepInEx/core/BepInEx.dll" \
  -r:"${GAME_BASE_DIR}/BepInEx/core/0Harmony.dll" \
  -r:"${GAME_BASE_DIR}/BepInEx/core/BepInEx.Harmony.dll" \
  `[ -e "${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/netstandard.dll" ] && echo "-r:\"${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/netstandard.dll\""` \
  -r:"${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/System.dll" \
  -r:"${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/System.Core.dll" \
  -r:"${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/UnityEngine.dll" \
  -r:"${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/UnityEngine.AIModule.dll" \
  -r:"${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/UnityEngine.CoreModule.dll" \
  -r:"${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/UnityEngine.UI.dll" \
  -r:"${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/mscorlib.dll" \
  $(for i in "${GAME_BASE_DIR}/${GAME_NAME}_Data/Managed/$ASSEMBLY"*.dll ; do echo -e "-r:\"$i\"\n" ; done) \
  -out:"${GAME_BASE_DIR}/BepInEx/plugins/${FILE_NAME%.*}".dll \
  -optimize $EXTRA_DEFINE \
  - $UTILS && rm -f "${GAME_BASE_DIR}/BepInEx/config/${PLUGIN_ID}.cfg";

if [ -n "$2" ]; then
    git add ${FILE_NAME}
    case $2 in
        R) git commit -am "`curl -s index.txt`" ;;
        r) git commit -am "`curl -s index.txt`" ;;
        RANDOM) git commit -am "`curl -s index.txt`" ;;
        random) git commit -am "`curl -s index.txt`" ;;
        U) git commit -am "`curl -s index.txt`" ;;
        u) git commit -am "`curl -s index.txt`" ;;
        UPLOAD) git commit -am "`curl -s index.txt`" ;;
        upload) git commit -am "`curl -s index.txt`" ;;
        *) git commit -am "$2" ;;
    esac
    git push
fi
exit





using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Collections.Generic;

using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using UnityEngine;

namespace %%NAMESPACE_ID%%;

[BepInPlugin("%%PLUGIN_ID%%", "%%NAMESPACE_ID%%", "0.1.0")]
public class Cheat : Neutron3529.ModEntry {
    public override void Awake() {
        base.Awake();
        // 主要逻辑放在`utils.cs`中,这里的Awake2只是为了以防万一
        // 目前来说,这个函数的唯一用途是用来“叮”……
        // 就像这样:
        logger("叮~修改器启动,请安心游戏");
    }
    [Desc("建筑不消耗电力")]
    class building_no_electric_cost : Entry {
        static IEnumerable<MethodBase> TargetMethods() {
            foreach(var ty in typeof(Mafi.Base.Prototypes.Transport.PortShapesData).Assembly.GetTypes()){
                foreach(MethodInfo i in ty.GetMethods(/*(BindingFlags)(-1)*/)){
                    if(i.Name=="RegisterData"){
                        yield return i;
                    }
                }
            }
        }
        static IEnumerable<CodeInstruction> Transpiler(MethodBase __originalMethod, IEnumerable<CodeInstruction> instructions) {
            logger("%%PLUGIN_ID%%-类"+System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name+"正在注入类别"+__originalMethod.DeclaringType.Name+"的"+__originalMethod.Name+"方法");
            instructions = new CodeMatcher(instructions)
                .MatchForward(false, // false = move at the start of the match, true = move at the end of the match
                    new CodeMatch(OpCodes.Call, typeof(Mafi.ElectricityExtensions).GetMethod("Kw",new Type[]{typeof(int)}))
                    // new CodeMatch(OpCodes.Call, typeof(Mafi.Base.Prototypes.Transport.PortShapesData).GetMethod("Kw",new Type[]{typeof(int)}))
                ).Repeat( matcher => // Do the following for each match
                    matcher
                    .InsertAndAdvance(
                        new CodeInstruction(OpCodes.Pop),
                        new CodeInstruction(OpCodes.Ldc_I4_0)
                    )
                    .Advance(1) // avoid conflict.
                ).InstructionEnumeration();
            return instructions;
        }
    }
    // [HarmonyPatch(typeof(Mafi.RecipeProtoBuilderExtensions),"AddOutput")]
    class machine_output_mult : Entry {
        [Desc("产量乘数")]
        static double factor=5;
        static IEnumerable<MethodBase> TargetMethods() {
            foreach(var ty in typeof(Mafi.Base.Prototypes.Transport.PortShapesData).Assembly.GetTypes()){
                foreach(MethodInfo i in ty.GetMethods(/*(BindingFlags)(-1)*/)){
                    if(i.Name=="RegisterData"){
                        yield return i;
                    }
                }
            }
        }
        static IEnumerable<CodeInstruction> Transpiler(MethodBase __originalMethod, IEnumerable<CodeInstruction> instructions) {
            logger("%%PLUGIN_ID%%-类"+System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name+"正在注入类别"+__originalMethod.DeclaringType.Name+"的"+__originalMethod.Name+"方法");
            // var from=typeof(Mafi.RecipeProtoBuilderExtensions).GetMethod("AddOutput",(BindingFlags)(-1));
            // var to=System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.GetMethod("AddOutput",(BindingFlags)(-1));
            instructions = new CodeMatcher(instructions)
                .MatchForward(false, // false = move at the start of the match, true = move at the end of the match
                    new CodeMatch(i=>i.opcode==OpCodes.Call && ((MethodBase)(i.operand)).Name == "AddOutput") // Generics
                ).Repeat( matcher => {// Do the following for each match
                    var x=((MethodInfo)(matcher.Instruction.operand)).GetGenericArguments();
                    if(x!=null){
                        var y=typeof(machine_output_mult).GetMethod("AddOutput",(BindingFlags)(-1)).MakeGenericMethod(x);
                        matcher.SetAndAdvance(
                            OpCodes.Call,y
                        ).Advance(1);
                        logwarn("from:"+x.ToString());
                        logwarn("to:"+y.ToString());
                    }
                }).InstructionEnumeration();
            return instructions;
        }
        static T AddOutput<T>(Mafi.Core.Factory.Recipes.IRecipeProtoBuilderState<T> builder, int quantity, Mafi.Core.Products.ProductProto.ID productId, string portSelector, bool outputAtStart, bool hideInUi)=>Mafi.RecipeProtoBuilderExtensions.AddOutput(builder, (int)(factor*(double)quantity), productId, portSelector, outputAtStart, hideInUi);
    }
}

utils.cs:

/*
 * Neutron3529's Unity Game Plugin
 * Copyright (C) 2022-2023 Neutron3529
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 * This program is a part of other mod, which could not be used directly
 * all of the bash script would append this file after the main .cs file
 * if you want to compile program manually, you should append it by yourself.
 */

using System;
using System.Reflection;
using System.Collections.Generic;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;

namespace Neutron3529;
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct | System.AttributeTargets.Field, AllowMultiple = false)]
public class Desc : System.Attribute {
    public string desc;
    public string str;
    public double val;
    public Desc(string desc,string str) {
        this.desc = desc;
        this.str = str;
        this.val = 0;
    }
    public Desc(string desc,double val) {
        this.desc = desc;
        this.str = "";
        this.val = val;
    }
    public Desc(string desc) {
        this.desc = desc;
        this.str = "";
        this.val = 0;
    }
    public bool Enable(ModEntry.Entry cls)=>this.val>=0; // for class desc only.
}
public class ModEntry : BaseUnityPlugin {
    public static HarmonyLib.Harmony harmony;
    public static BepInEx.Configuration.ConfigFile config;
#if DEBUG
#if VERBOSE
    public static Action<string> vlogger;
#endif
    public static Action<string> logger;
#else
    public static void vlogger(string s){}
    public static void logger(string s){}
#endif
    public static Action<string> logwarn;
    public static void logexception(Exception e){
        logwarn(_strexception(e,0));
    }
    static string _strexception(Exception e, int depth){
        var padding=depth==0?"\n":"\n"+new string(' ',depth*4);
        var content=("\n"+e.GetType().Name+": "+e.Message+"\n"+e.StackTrace).Replace("\n",padding);
        if(e.InnerException !=null){
            return content+_strexception(e.InnerException,depth+1);
        } else {
            return content;
        }
    }
    public abstract class Entry {
        public bool enable=false;
        public string full_desc="";
        Dictionary<FieldInfo, object> dict=new Dictionary<FieldInfo, object>();
        public virtual bool assign(FieldInfo f, Desc desc){ // return whether the type is enabled.
            string full_entry = this.GetType().Name+"."+f.Name;
            switch(Type.GetTypeCode(f.FieldType)) {
                case TypeCode.SByte:
                    dict[f]=config.Bind("config",full_entry,(sbyte)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<sbyte>)dict[f]).Value));
                    return (((ConfigEntry<sbyte>)dict[f]).Value)>=0;
                case TypeCode.Byte:
                    dict[f]=config.Bind("config",full_entry,(byte)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<byte>)dict[f]).Value));
                    return (((ConfigEntry<byte>)dict[f]).Value)>0;
                case TypeCode.Int16:
                    dict[f]=config.Bind("config",full_entry,(short)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<short>)dict[f]).Value));
                    return (((ConfigEntry<short>)dict[f]).Value)>=0;
                case TypeCode.UInt16:
                    dict[f]=config.Bind("config",full_entry,(ushort)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<ushort>)dict[f]).Value));
                    return (((ConfigEntry<ushort>)dict[f]).Value)>0;
                case TypeCode.Int32:
                    dict[f]=config.Bind("config",full_entry,(int)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<int>)dict[f]).Value));
                    return (((ConfigEntry<int>)dict[f]).Value)>=0;
                case TypeCode.UInt32:
                    dict[f]=config.Bind("config",full_entry,(uint)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<uint>)dict[f]).Value));
                    return (((ConfigEntry<uint>)dict[f]).Value)>0;
                case TypeCode.Single:
                    dict[f]=config.Bind("config",full_entry,(float)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<float>)dict[f]).Value));
                    return (((ConfigEntry<float>)dict[f]).Value)>=0;
                case TypeCode.Double:
                    dict[f]=config.Bind("config",full_entry,(double)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<double>)dict[f]).Value));
                    return (((ConfigEntry<double>)dict[f]).Value)>=0;
                case TypeCode.Boolean:
                    dict[f]=config.Bind("config",full_entry,(bool)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<bool>)dict[f]).Value));
                    return (((ConfigEntry<bool>)dict[f]).Value);
                case TypeCode.String:
                    dict[f]=config.Bind("config",full_entry,(string)(f.GetValue(this)),desc.desc);
                    f.SetValue(this,(((ConfigEntry<string>)dict[f]).Value));
                    return ((ConfigEntry<string>)dict[f]).Value.Length>1;
                default:logwarn($"Unsupport type: {f.Name} : {f.FieldType}");return true;
            }
        }
        public virtual void Init(){
#if VERBOSE
            vlogger("正在修改:"+this.GetType().Name);
#endif
            var type = this.GetType();
            this.full_desc="";
            Desc dsc = (Desc) Attribute.GetCustomAttribute(type, typeof(Desc));
            if(dsc!=null){
                this.enable=dsc.Enable(this);
                if((bool)(type.GetMethod("assign").Invoke(this,new object[]{type.GetField("enable"),dsc}))){
                    full_desc=dsc.desc;
                }
            }
            foreach(var f in type.GetFields((BindingFlags)(-1))){
                Desc desc = (Desc) Attribute.GetCustomAttribute(f, typeof(Desc));
#if VERBOSE
                vlogger("正在检查:"+this.GetType().Name+"的"+f.Name+"字段");
#endif
                if(desc!=null){
#if VERBOSE
                    vlogger("正在检查:"+this.GetType().Name+"的"+f.Name+"字段--包含desc信息");
#endif
                    enable|=(bool)(type.GetMethod("assign").Invoke(this,new object[]{f,desc}));
                    if(dsc==null){
                        if(full_desc.Length>0){
                            full_desc=full_desc+" & "+desc.desc;
                        }else{
                            full_desc=desc.desc;
                        }
                    }
                }
            }
            if(enable){
                this.Enable();
                logger($"已启用:{full_desc}");
            } else {
                logger($"未启用:{full_desc}");
            }
        }
        public virtual void Enable(){
            harmony.PatchAll(this.GetType());
        }
    }
    public virtual void Awake() {
        logwarn=Logger.LogWarning;
        Logger.LogInfo("此Mod使用AGPL-v3许可发布,如果你使用了这里的代码,请按照相同许可发布你修改后的mod。");
        harmony=new Harmony("%%PLUGIN_ID%%");   // `harmony` is defined in utils.cs
        config=Config;                          // `utils`   is defined in utils.cs
#if DEBUG
        logger=Logger.LogInfo;                  // `logger`  is defined in utils.cs
#if VERBOSE
        vlogger=Logger.LogWarning;              // `vlogger` is defined in utils.cs
#endif
#endif
        logger("开始注入");
        Type[] t=new Type[0];
        foreach(var type in typeof(ModEntry).Module.GetTypes()){
            try{
#if VERBOSE
                vlogger("搜索到:"+type.ToString());
#endif
                if(type.IsSubclassOf(typeof(Entry))){
                    object entry=type.GetConstructor(t).Invoke(t);
                    if (entry!=null){
                        type.GetMethod("Init",t).Invoke(entry,t);
                    }else{logwarn("Entry:"+type+"的type.GetConstructor().Invoke()是null,这多半是mod出了问题,如果你看到这个,请联系mod作者。");}
                }
            } catch (Exception e) {
                logwarn("Mod出现了意料之外的错误,请及时与Mod作者联系。");
                logexception(e);
            }
        }
    }
}

compile the program with BepInEx 5.4.21.0, install the Mod and restart the game.

then the localization would lock on English version no matter what localization you choose.

I don't read the EULA very carefully, thus if it is designed to be part of anti-cheat system. I am very sorry.

Game version

v0.5.0B

Files not imported: Mod.zip

1 attachment only for registered users
19 Showing 1–1 of 1