mirror of
https://github.com/Alex-Rachel/TEngine.git
synced 2025-08-14 16:51:28 +00:00
Merge pull request #32 from ALEXTANGXIAO/TEngine_v_3.0.0
TEngine v 3.0.0 [+]HybridCLR
This commit is contained in:
21
Assets/GameScripts/HotFix/BattleCore/Empty.cs
Normal file
21
Assets/GameScripts/HotFix/BattleCore/Empty.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BattleCore.Runtime
|
||||
{
|
||||
public class Empty : MonoBehaviour
|
||||
{
|
||||
// Start is called before the first frame update
|
||||
void Start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/GameScripts/HotFix/BattleCore/Empty.cs.meta
Normal file
11
Assets/GameScripts/HotFix/BattleCore/Empty.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fc394f8c6ad5304a82ff84b4263756f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/GameScripts/HotFix/GameProto/GameProtocol.meta
Normal file
8
Assets/GameScripts/HotFix/GameProto/GameProtocol.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86ce21ac2bf6d5543b8b0cc7f1972046
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,68 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using ProtoBuf;
|
||||
using TEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GameProto
|
||||
{
|
||||
// 这个文件只放协议,和协议头
|
||||
// 消息协议
|
||||
[Serializable,global::ProtoBuf.ProtoContract(Name = @"CSPkg")]
|
||||
public partial class CSPkg
|
||||
{
|
||||
[global::ProtoBuf.ProtoMember(1)]
|
||||
public CSPkgHead Head { get; set; }
|
||||
|
||||
[global::ProtoBuf.ProtoMember(2)]
|
||||
public CSPkgBody Body { get; set; }
|
||||
|
||||
}
|
||||
|
||||
// 消息协议头
|
||||
[Serializable,global::ProtoBuf.ProtoContract(Name = @"CSPkgHead")]
|
||||
public partial class CSPkgHead
|
||||
{
|
||||
[global::ProtoBuf.ProtoMember(1)]
|
||||
public uint MsgId { get; set; }
|
||||
|
||||
[global::ProtoBuf.ProtoMember(2)]
|
||||
public uint MsgLength { get; set; }
|
||||
|
||||
[global::ProtoBuf.ProtoMember(3)]
|
||||
public uint MsgVersion { get; set; }
|
||||
|
||||
[global::ProtoBuf.ProtoMember(4)]
|
||||
public uint Echo { get; set; }
|
||||
|
||||
[global::ProtoBuf.ProtoMember(5)]
|
||||
public uint SvrTime { get; set; }
|
||||
|
||||
}
|
||||
|
||||
// 消息协议体
|
||||
[Serializable,global::ProtoBuf.ProtoContract(Name = @"CSPkgBody")]
|
||||
public partial class CSPkgBody
|
||||
{
|
||||
}
|
||||
|
||||
// 协议ID
|
||||
[global::ProtoBuf.ProtoContract()]
|
||||
public enum CSMsgID
|
||||
{
|
||||
CS_START = 0,
|
||||
|
||||
CS_HeartBeat = 10001,
|
||||
|
||||
CS_END = 10000,
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d59eaaad20d9234ba941173b39361fc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
#if HybridCLR_Enable
|
||||
using HybridCLR;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
using TEngine;
|
||||
using System.Reflection;
|
||||
@@ -21,287 +19,264 @@ namespace GameMain
|
||||
/// </summary>
|
||||
public bool NeedLoadDll => GameModule.Resource.playMode == EPlayMode.HostPlayMode || GameModule.Resource.playMode == EPlayMode.OfflinePlayMode;
|
||||
|
||||
public override bool UseNativeDialog => true;
|
||||
// private int m_LoadAssetCount;
|
||||
// private int m_LoadMetadataAssetCount;
|
||||
// private int m_FailureAssetCount;
|
||||
// private int m_FailureMetadataAssetCount;
|
||||
// private bool m_LoadAssemblyComplete;
|
||||
// private bool m_LoadMetadataAssemblyComplete;
|
||||
// private bool m_LoadAssemblyWait;
|
||||
// private bool m_LoadMetadataAssemblyWait;
|
||||
// private System.Reflection.Assembly m_MainLogicAssembly;
|
||||
// private List<System.Reflection.Assembly> m_HotfixAssemblys;
|
||||
// private IFsm<IProcedureManager> m_procedureOwner;
|
||||
//
|
||||
// protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
|
||||
// {
|
||||
// base.OnEnter(procedureOwner);
|
||||
// Log.Debug("HyBridCLR ProcedureLoadAssembly OnEnter");
|
||||
// m_procedureOwner = procedureOwner;
|
||||
// m_LoadAssemblyComplete = false;
|
||||
// m_HotfixAssemblys = new List<Assembly>();
|
||||
//
|
||||
// if (!NeedLoadDll || GameModule.Resource.playMode == EPlayMode.EditorSimulateMode)
|
||||
// {
|
||||
// m_MainLogicAssembly = GetMainLogicAssembly();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (SettingsUtils.HybridCLRCustomGlobalSettings.Enable)
|
||||
// {
|
||||
// m_LoadAssetCallbacks ??= new LoadAssetCallbacks(LoadAssetSuccess, LoadAssetFailure);
|
||||
// foreach (var hotUpdateDllName in SettingsUtils.HybridCLRCustomGlobalSettings.HotUpdateAssemblies)
|
||||
// {
|
||||
// var assetPath = Utility.Path.GetRegularPath(
|
||||
// Path.Combine(
|
||||
// "Assets",
|
||||
// SettingsUtils.HybridCLRCustomGlobalSettings.AssemblyTextAssetPath,
|
||||
// $"{hotUpdateDllName}{SettingsUtils.HybridCLRCustomGlobalSettings.AssemblyTextAssetExtension}"));
|
||||
// Log.Debug($"LoadAsset: [ {assetPath} ]");
|
||||
// m_LoadAssetCount++;
|
||||
// GameModule.Resource.LoadAssetAsync(assetPath,typeof(UnityEngine.TextAsset), m_LoadAssetCallbacks, hotUpdateDllName);
|
||||
// }
|
||||
//
|
||||
// m_LoadAssemblyWait = true;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// m_MainLogicAssembly = GetMainLogicAssembly();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (SettingsUtils.HybridCLRCustomGlobalSettings.Enable)
|
||||
// {
|
||||
// #if !UNITY_EDITOR
|
||||
// m_LoadMetadataAssemblyComplete = false;
|
||||
// LoadMetadataForAOTAssembly();
|
||||
// #else
|
||||
// m_LoadMetadataAssemblyComplete = true;
|
||||
// #endif
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// m_LoadMetadataAssemblyComplete = true;
|
||||
// }
|
||||
//
|
||||
// if (m_LoadAssetCount == 0)
|
||||
// {
|
||||
// m_LoadAssemblyComplete = true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds, float realElapseSeconds)
|
||||
// {
|
||||
// base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
|
||||
// if (!m_LoadAssemblyComplete)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
// if (!m_LoadMetadataAssemblyComplete)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
// AllAssemblyLoadComplete();
|
||||
// }
|
||||
//
|
||||
// private void AllAssemblyLoadComplete()
|
||||
// {
|
||||
// if (GameModule.Resource.playMode == EPlayMode.EditorSimulateMode)
|
||||
// {
|
||||
// ChangeState<ProcedureStartGame>(m_procedureOwner);
|
||||
// return;
|
||||
// }
|
||||
// if (m_MainLogicAssembly == null)
|
||||
// {
|
||||
// Log.Fatal($"Main logic assembly missing.");
|
||||
// return;
|
||||
// }
|
||||
// var appType = m_MainLogicAssembly.GetType("GameMain");
|
||||
// if (appType == null)
|
||||
// {
|
||||
// Log.Fatal($"Main logic type 'GameMain' missing.");
|
||||
// return;
|
||||
// }
|
||||
// var entryMethod = appType.GetMethod("Entrance");
|
||||
// if (entryMethod == null)
|
||||
// {
|
||||
// Log.Fatal($"Main logic entry method 'Entrance' missing.");
|
||||
// return;
|
||||
// }
|
||||
// object[] objects = new object[] { new object[] { m_HotfixAssemblys } };
|
||||
// entryMethod.Invoke(appType, objects);
|
||||
// ChangeState<ProcedureStartGame>(m_procedureOwner);
|
||||
// }
|
||||
//
|
||||
// private Assembly GetMainLogicAssembly()
|
||||
// {
|
||||
// Assembly mainLogicAssembly = null;
|
||||
// foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
// {
|
||||
// if (string.Compare(SettingsUtils.HybridCLRCustomGlobalSettings.LogicMainDllName, $"{assembly.GetName().Name}.dll",
|
||||
// StringComparison.Ordinal) == 0)
|
||||
// {
|
||||
// mainLogicAssembly = assembly;
|
||||
// }
|
||||
//
|
||||
// foreach (var hotUpdateDllName in SettingsUtils.HybridCLRCustomGlobalSettings.HotUpdateAssemblies)
|
||||
// {
|
||||
// if (hotUpdateDllName == $"{assembly.GetName().Name}.dll")
|
||||
// {
|
||||
// m_HotfixAssemblys.Add(assembly);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (mainLogicAssembly != null && m_HotfixAssemblys.Count == SettingsUtils.HybridCLRCustomGlobalSettings.HotUpdateAssemblies.Count)
|
||||
// {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return mainLogicAssembly;
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 加载代码资源成功回调。
|
||||
// /// </summary>
|
||||
// /// <param name="assetName">资源名称。</param>
|
||||
// /// <param name="asset">资源实例。</param>
|
||||
// /// <param name="duration">加载耗时。</param>
|
||||
// /// <param name="userData">用户数据。</param>
|
||||
// private void LoadAssetSuccess(string assetName, object asset, float duration, object userData)
|
||||
// {
|
||||
// m_LoadAssetCount--;
|
||||
// Log.Debug($"LoadAssetSuccess, assetName: [ {assetName} ], duration: [ {duration} ], userData: [ {userData} ]");
|
||||
// var textAsset = asset as TextAsset;
|
||||
// if (textAsset == null)
|
||||
// {
|
||||
// Log.Warning($"Load text asset [ {assetName} ] failed.");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// var assembly = Assembly.Load(textAsset.bytes);
|
||||
// if (string.Compare(SettingsUtils.HybridCLRCustomGlobalSettings.LogicMainDllName, userData as string, StringComparison.Ordinal) == 0)
|
||||
// {
|
||||
// m_MainLogicAssembly = assembly;
|
||||
// }
|
||||
// m_HotfixAssemblys.Add(assembly);
|
||||
// Log.Debug($"Assembly [ {assembly.GetName().Name} ] loaded");
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// m_FailureAssetCount++;
|
||||
// Log.Fatal(e);
|
||||
// throw;
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// m_LoadAssemblyComplete = m_LoadAssemblyWait && 0 == m_LoadAssetCount;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 加载代码资源失败回调。
|
||||
// /// </summary>
|
||||
// /// <param name="assetName">资源名称。</param>
|
||||
// /// <param name="status">加载状态。</param>
|
||||
// /// <param name="errorMessage">错误信息。</param>
|
||||
// /// <param name="userData">自定义数据。</param>
|
||||
// private void LoadAssetFailure(string assetName, LoadResourceStatus status, string errorMessage, object userData)
|
||||
// {
|
||||
// Log.Fatal($"LoadAssetFailure, assetName: [ {assetName} ], status: [ {status} ], errorMessage: [ {errorMessage} ], userData: [ {userData} ]");
|
||||
// m_LoadAssetCount--;
|
||||
// m_FailureAssetCount++;
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 为Aot Assembly加载原始metadata, 这个代码放Aot或者热更新都行。
|
||||
// /// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行。
|
||||
// /// </summary>
|
||||
// public void LoadMetadataForAOTAssembly()
|
||||
// {
|
||||
// // 可以加载任意aot assembly的对应的dll。但要求dll必须与unity build过程中生成的裁剪后的dll一致,而不能直接使用原始dll。
|
||||
// // 我们在BuildProcessor_xxx里添加了处理代码,这些裁剪后的dll在打包时自动被复制到 {项目目录}/HybridCLRData/AssembliesPostIl2CppStrip/{Target} 目录。
|
||||
//
|
||||
// // 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
|
||||
// // 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
|
||||
// if (SettingsUtils.HybridCLRCustomGlobalSettings.AOTMetaAssemblies.Count == 0)
|
||||
// {
|
||||
// m_LoadMetadataAssemblyComplete = true;
|
||||
// return;
|
||||
// }
|
||||
// m_LoadMetadataAssetCallbacks ??= new LoadAssetCallbacks(LoadMetadataAssetSuccess, LoadMetadataAssetFailure);
|
||||
// foreach (var aotDllName in SettingsUtils.HybridCLRCustomGlobalSettings.AOTMetaAssemblies)
|
||||
// {
|
||||
// var assetPath = Utility.Path.GetRegularPath(
|
||||
// Path.Combine(
|
||||
// "Assets",
|
||||
// SettingsUtils.HybridCLRCustomGlobalSettings.AssemblyTextAssetPath,
|
||||
// $"{aotDllName}{SettingsUtils.HybridCLRCustomGlobalSettings.AssemblyTextAssetExtension}"));
|
||||
// Log.Debug($"LoadMetadataAsset: [ {assetPath} ]");
|
||||
// m_LoadMetadataAssetCount++;
|
||||
// GameModule.Resource.LoadAssetAsync(assetPath,typeof(UnityEngine.TextAsset), m_LoadMetadataAssetCallbacks, aotDllName);
|
||||
// }
|
||||
// m_LoadMetadataAssemblyWait = true;
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 加载元数据资源成功回调。
|
||||
// /// </summary>
|
||||
// /// <param name="assetName">资源名称。</param>
|
||||
// /// <param name="asset">资源实例。</param>
|
||||
// /// <param name="duration">加载耗时。</param>
|
||||
// /// <param name="userData">用户数据。</param>
|
||||
// private unsafe void LoadMetadataAssetSuccess(string assetName, object asset, float duration, object userData)
|
||||
// {
|
||||
// m_LoadMetadataAssetCount--;
|
||||
// Log.Debug($"LoadMetadataAssetSuccess, assetName: [ {assetName} ], duration: [ {duration} ], userData: [ {userData} ]");
|
||||
// var textAsset = asset as TextAsset;
|
||||
// if (null == textAsset)
|
||||
// {
|
||||
// Log.Debug($"LoadMetadataAssetSuccess:Load text asset [ {assetName} ] failed.");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// byte[] dllBytes = textAsset.bytes;
|
||||
// fixed (byte* ptr = dllBytes)
|
||||
// {
|
||||
// #if HybridCLR_Enable
|
||||
// // 加载assembly对应的dll,会自动为它hook。一旦Aot泛型函数的native函数不存在,用解释器版本代码
|
||||
// HomologousImageMode mode = HomologousImageMode.SuperSet;
|
||||
// LoadImageErrorCode err = (LoadImageErrorCode)HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(dllBytes,mode);
|
||||
// Log.Warning($"LoadMetadataForAOTAssembly:{userData as string}. mode:{mode} ret:{err}");
|
||||
// #endif
|
||||
// }
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// m_FailureMetadataAssetCount++;
|
||||
// Log.Fatal(e.Message);
|
||||
// throw;
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// m_LoadMetadataAssemblyComplete = m_LoadMetadataAssemblyWait && 0 == m_LoadMetadataAssetCount;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 加载元数据资源失败回调。
|
||||
// /// </summary>
|
||||
// /// <param name="assetName">资源名称。</param>
|
||||
// /// <param name="status">加载状态。</param>
|
||||
// /// <param name="errorMessage">错误信息。</param>
|
||||
// /// <param name="userData">自定义数据。</param>
|
||||
// private void LoadMetadataAssetFailure(string assetName, LoadResourceStatus status, string errorMessage, object userData)
|
||||
// {
|
||||
// Log.Warning($"LoadAssetFailure, assetName: [ {assetName} ], status: [ {status} ], errorMessage: [ {errorMessage} ], userData: [ {userData} ]");
|
||||
// m_LoadMetadataAssetCount--;
|
||||
// m_FailureMetadataAssetCount++;
|
||||
// }
|
||||
private bool m_enableAddressable = true;
|
||||
public override bool UseNativeDialog => true;
|
||||
private int m_LoadAssetCount;
|
||||
private int m_LoadMetadataAssetCount;
|
||||
private int m_FailureAssetCount;
|
||||
private int m_FailureMetadataAssetCount;
|
||||
private bool m_LoadAssemblyComplete;
|
||||
private bool m_LoadMetadataAssemblyComplete;
|
||||
private bool m_LoadAssemblyWait;
|
||||
private bool m_LoadMetadataAssemblyWait;
|
||||
private Assembly m_MainLogicAssembly;
|
||||
private List<Assembly> m_HotfixAssemblys;
|
||||
private IFsm<IProcedureManager> m_procedureOwner;
|
||||
|
||||
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
|
||||
{
|
||||
base.OnEnter(procedureOwner);
|
||||
Log.Debug("HyBridCLR ProcedureLoadAssembly OnEnter");
|
||||
m_procedureOwner = procedureOwner;
|
||||
m_LoadAssemblyComplete = false;
|
||||
m_HotfixAssemblys = new List<Assembly>();
|
||||
|
||||
if (!NeedLoadDll || GameModule.Resource.playMode == EPlayMode.EditorSimulateMode)
|
||||
{
|
||||
m_MainLogicAssembly = GetMainLogicAssembly();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SettingsUtils.HybridCLRCustomGlobalSettings.Enable)
|
||||
{
|
||||
foreach (string hotUpdateDllName in SettingsUtils.HybridCLRCustomGlobalSettings.HotUpdateAssemblies)
|
||||
{
|
||||
var assetLocation = hotUpdateDllName;
|
||||
if (!m_enableAddressable)
|
||||
{
|
||||
assetLocation = Utility.Path.GetRegularPath(
|
||||
Path.Combine(
|
||||
"Assets",
|
||||
SettingsUtils.HybridCLRCustomGlobalSettings.AssemblyTextAssetPath,
|
||||
$"{hotUpdateDllName}{SettingsUtils.HybridCLRCustomGlobalSettings.AssemblyTextAssetExtension}"));
|
||||
}
|
||||
|
||||
Log.Debug($"LoadAsset: [ {assetLocation} ]");
|
||||
m_LoadAssetCount++;
|
||||
GameModule.Resource.LoadAssetAsync<TextAsset>(assetLocation,LoadAssetSuccess);
|
||||
}
|
||||
|
||||
m_LoadAssemblyWait = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_MainLogicAssembly = GetMainLogicAssembly();
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsUtils.HybridCLRCustomGlobalSettings.Enable)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
m_LoadMetadataAssemblyComplete = false;
|
||||
LoadMetadataForAOTAssembly();
|
||||
#else
|
||||
m_LoadMetadataAssemblyComplete = true;
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
m_LoadMetadataAssemblyComplete = true;
|
||||
}
|
||||
|
||||
if (m_LoadAssetCount == 0)
|
||||
{
|
||||
m_LoadAssemblyComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
|
||||
if (!m_LoadAssemblyComplete)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!m_LoadMetadataAssemblyComplete)
|
||||
{
|
||||
return;
|
||||
}
|
||||
AllAssemblyLoadComplete();
|
||||
}
|
||||
|
||||
private void AllAssemblyLoadComplete()
|
||||
{
|
||||
if (GameModule.Resource.playMode == EPlayMode.EditorSimulateMode)
|
||||
{
|
||||
ChangeState<ProcedureStartGame>(m_procedureOwner);
|
||||
return;
|
||||
}
|
||||
if (m_MainLogicAssembly == null)
|
||||
{
|
||||
Log.Fatal($"Main logic assembly missing.");
|
||||
return;
|
||||
}
|
||||
var appType = m_MainLogicAssembly.GetType("GameApp");
|
||||
if (appType == null)
|
||||
{
|
||||
Log.Fatal($"Main logic type 'GameMain' missing.");
|
||||
return;
|
||||
}
|
||||
var entryMethod = appType.GetMethod("Entrance");
|
||||
if (entryMethod == null)
|
||||
{
|
||||
Log.Fatal($"Main logic entry method 'Entrance' missing.");
|
||||
return;
|
||||
}
|
||||
object[] objects = new object[] { new object[] { m_HotfixAssemblys } };
|
||||
entryMethod.Invoke(appType, objects);
|
||||
ChangeState<ProcedureStartGame>(m_procedureOwner);
|
||||
}
|
||||
|
||||
private Assembly GetMainLogicAssembly()
|
||||
{
|
||||
Assembly mainLogicAssembly = null;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (string.Compare(SettingsUtils.HybridCLRCustomGlobalSettings.LogicMainDllName, $"{assembly.GetName().Name}.dll",
|
||||
StringComparison.Ordinal) == 0)
|
||||
{
|
||||
mainLogicAssembly = assembly;
|
||||
}
|
||||
|
||||
foreach (var hotUpdateDllName in SettingsUtils.HybridCLRCustomGlobalSettings.HotUpdateAssemblies)
|
||||
{
|
||||
if (hotUpdateDllName == $"{assembly.GetName().Name}.dll")
|
||||
{
|
||||
m_HotfixAssemblys.Add(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
if (mainLogicAssembly != null && m_HotfixAssemblys.Count == SettingsUtils.HybridCLRCustomGlobalSettings.HotUpdateAssemblies.Count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return mainLogicAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载代码资源成功回调。
|
||||
/// </summary>
|
||||
/// <param name="assetOperationHandle">资源操作句柄。</param>
|
||||
private void LoadAssetSuccess(AssetOperationHandle assetOperationHandle)
|
||||
{
|
||||
m_LoadAssetCount--;
|
||||
var assetName = assetOperationHandle.AssetObject.name;
|
||||
Log.Debug($"LoadAssetSuccess, assetName: [ {assetName} ]");
|
||||
|
||||
var textAsset = assetOperationHandle.AssetObject as TextAsset;
|
||||
if (textAsset == null)
|
||||
{
|
||||
Log.Warning($"Load text asset [ {assetName} ] failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.Load(textAsset.bytes);
|
||||
if (string.Compare(SettingsUtils.HybridCLRCustomGlobalSettings.LogicMainDllName, assetName, StringComparison.Ordinal) == 0)
|
||||
{
|
||||
m_MainLogicAssembly = assembly;
|
||||
}
|
||||
m_HotfixAssemblys.Add(assembly);
|
||||
Log.Debug($"Assembly [ {assembly.GetName().Name} ] loaded");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_FailureAssetCount++;
|
||||
Log.Fatal(e);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_LoadAssemblyComplete = m_LoadAssemblyWait && 0 == m_LoadAssetCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为Aot Assembly加载原始metadata, 这个代码放Aot或者热更新都行。
|
||||
/// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行。
|
||||
/// </summary>
|
||||
public void LoadMetadataForAOTAssembly()
|
||||
{
|
||||
// 可以加载任意aot assembly的对应的dll。但要求dll必须与unity build过程中生成的裁剪后的dll一致,而不能直接使用原始dll。
|
||||
// 我们在BuildProcessor_xxx里添加了处理代码,这些裁剪后的dll在打包时自动被复制到 {项目目录}/HybridCLRData/AssembliesPostIl2CppStrip/{Target} 目录。
|
||||
|
||||
// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
|
||||
// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
|
||||
if (SettingsUtils.HybridCLRCustomGlobalSettings.AOTMetaAssemblies.Count == 0)
|
||||
{
|
||||
m_LoadMetadataAssemblyComplete = true;
|
||||
return;
|
||||
}
|
||||
foreach (string aotDllName in SettingsUtils.HybridCLRCustomGlobalSettings.AOTMetaAssemblies)
|
||||
{
|
||||
var assetLocation = aotDllName;
|
||||
if (!m_enableAddressable)
|
||||
{
|
||||
assetLocation = Utility.Path.GetRegularPath(
|
||||
Path.Combine(
|
||||
"Assets",
|
||||
SettingsUtils.HybridCLRCustomGlobalSettings.AssemblyTextAssetPath,
|
||||
$"{aotDllName}{SettingsUtils.HybridCLRCustomGlobalSettings.AssemblyTextAssetExtension}"));
|
||||
}
|
||||
|
||||
|
||||
Log.Debug($"LoadMetadataAsset: [ {assetLocation} ]");
|
||||
m_LoadMetadataAssetCount++;
|
||||
GameModule.Resource.LoadAssetAsync<TextAsset>(assetLocation,LoadMetadataAssetSuccess);
|
||||
}
|
||||
m_LoadMetadataAssemblyWait = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载元数据资源成功回调。
|
||||
/// </summary>
|
||||
/// <param name="assetOperationHandle">资源操作句柄。</param>
|
||||
private unsafe void LoadMetadataAssetSuccess(AssetOperationHandle assetOperationHandle)
|
||||
{
|
||||
m_LoadMetadataAssetCount--;
|
||||
string assetName = assetOperationHandle.AssetObject.name;
|
||||
Log.Debug($"LoadMetadataAssetSuccess, assetName: [ {assetName} ]");
|
||||
var textAsset = assetOperationHandle.AssetObject as TextAsset;
|
||||
if (null == textAsset)
|
||||
{
|
||||
Log.Debug($"LoadMetadataAssetSuccess:Load text asset [ {assetName} ] failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
byte[] dllBytes = textAsset.bytes;
|
||||
fixed (byte* ptr = dllBytes)
|
||||
{
|
||||
// 加载assembly对应的dll,会自动为它hook。一旦Aot泛型函数的native函数不存在,用解释器版本代码
|
||||
HomologousImageMode mode = HomologousImageMode.SuperSet;
|
||||
LoadImageErrorCode err = (LoadImageErrorCode)HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(dllBytes,mode);
|
||||
Log.Warning($"LoadMetadataForAOTAssembly:{assetName}. mode:{mode} ret:{err}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_FailureMetadataAssetCount++;
|
||||
Log.Fatal(e.Message);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_LoadMetadataAssemblyComplete = m_LoadMetadataAssemblyWait && 0 == m_LoadMetadataAssetCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
/*
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using HybridCLR.Editor;
|
||||
@@ -58,7 +57,4 @@ public static class BuildAssetsCommand
|
||||
Debug.Log($"[CopyHotUpdateAssembliesToStreamingAssets] copy hotfix dll {dllPath} -> {dllBytesPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
}
|
8
Assets/TEngine/Editor/ProtoGenTools.meta
Normal file
8
Assets/TEngine/Editor/ProtoGenTools.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad601c9f244a8ad4ea31b17228958eeb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
291
Assets/TEngine/Editor/ProtoGenTools/ProtoGenTools.cs
Normal file
291
Assets/TEngine/Editor/ProtoGenTools/ProtoGenTools.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace TEngine.Editor
|
||||
{
|
||||
public static class ProtoGenTools
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
[MenuItem("TEngine/生成Proto|Gen Proto", false, 10)]
|
||||
#endif
|
||||
public static void Export()
|
||||
{
|
||||
InnerProto2CS.Proto2CS();
|
||||
Debug.Log("proto2cs succeed!");
|
||||
}
|
||||
}
|
||||
|
||||
public static class InnerProto2CS
|
||||
{
|
||||
private static string ProtoPath = UnityEngine.Application.dataPath + "/..\\Luban\\Proto\\pb_schemas\\";
|
||||
|
||||
private static string OutPutPath =
|
||||
UnityEngine.Application.dataPath + "/..\\Luban\\Proto\\Gen\\";
|
||||
|
||||
private static readonly char[] splitChars = { ' ', '\t' };
|
||||
|
||||
public static void Proto2CS()
|
||||
{
|
||||
Proto2CS("GameProto", "ProtoBase.proto", OutPutPath,10001,false);
|
||||
}
|
||||
|
||||
public static void Proto2CS(string nameSpace, string protoName, string outputPath, int startOpcode,bool useMemoryPool = false)
|
||||
{
|
||||
if (!Directory.Exists(outputPath))
|
||||
{
|
||||
Directory.CreateDirectory(outputPath);
|
||||
}
|
||||
|
||||
string proto = Path.Combine(ProtoPath, protoName);
|
||||
string csPath = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(proto) + ".cs");
|
||||
|
||||
string s = File.ReadAllText(proto);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append("//------------------------------------------------------------------------------\n");
|
||||
sb.Append("// <auto-generated>\n");
|
||||
sb.Append("// This code was generated by a tool.\n");
|
||||
sb.Append("// Changes to this file may cause incorrect behavior and will be lost if\n");
|
||||
sb.Append("// the code is regenerated.\n");
|
||||
sb.Append("// </auto-generated>\n");
|
||||
sb.Append("//------------------------------------------------------------------------------\n");
|
||||
sb.Append("\n");
|
||||
sb.Append("using System;\n");
|
||||
sb.Append("using ProtoBuf;\n");
|
||||
sb.Append("using TEngine;\n");
|
||||
sb.Append("using System.Collections.Generic;\n");
|
||||
sb.Append("\n");
|
||||
sb.Append($"namespace {nameSpace}\n");
|
||||
sb.Append("{\n");
|
||||
|
||||
bool isMsgStart = false;
|
||||
bool isEnumStart = false;
|
||||
foreach (string line in s.Split('\n'))
|
||||
{
|
||||
string newline = line.Trim();
|
||||
|
||||
if (newline == "")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newline.StartsWith("//ResponseType"))
|
||||
{
|
||||
string responseType = line.Split(' ')[1].TrimEnd('\r', '\n');
|
||||
sb.AppendLine($"\t[ResponseType(nameof({responseType}))]");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newline.StartsWith("//"))
|
||||
{
|
||||
sb.Append($"\t{newline}\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newline.StartsWith("message"))
|
||||
{
|
||||
string parentClass = "";
|
||||
isMsgStart = true;
|
||||
string msgName = newline.Split(splitChars, StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
string[] ss = newline.Split(new[] { "//" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (ss.Length == 2)
|
||||
{
|
||||
parentClass = ss[1].Trim();
|
||||
}
|
||||
|
||||
sb.Append($"\t[Serializable,global::ProtoBuf.ProtoContract(Name = @\"{msgName}\")]\n");
|
||||
if (useMemoryPool)
|
||||
{
|
||||
sb.Append($"\tpublic partial class {msgName}: IMemory");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"\tpublic partial class {msgName}");
|
||||
}
|
||||
if (parentClass != "")
|
||||
{
|
||||
sb.Append($", {parentClass}\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("\n");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isMsgStart)
|
||||
{
|
||||
if (newline == "{")
|
||||
{
|
||||
sb.Append("\t{\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newline == "}")
|
||||
{
|
||||
isMsgStart = false;
|
||||
sb.Append("\t}\n\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newline.Trim().StartsWith("//"))
|
||||
{
|
||||
sb.AppendLine(newline);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newline.Trim() != "" && newline != "}")
|
||||
{
|
||||
if (newline.StartsWith("repeated"))
|
||||
{
|
||||
Repeated(sb, nameSpace, newline);
|
||||
}
|
||||
else
|
||||
{
|
||||
Members(sb, newline, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (newline.StartsWith("enum"))
|
||||
{
|
||||
isEnumStart = true;
|
||||
string enumName = newline.Split(splitChars, StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
|
||||
sb.Append($"\t[global::ProtoBuf.ProtoContract()]\n");
|
||||
sb.Append($"\tpublic enum {enumName}");
|
||||
sb.Append("\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isEnumStart)
|
||||
{
|
||||
if (newline == "{")
|
||||
{
|
||||
sb.Append("\t{\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newline == "}")
|
||||
{
|
||||
isEnumStart = false;
|
||||
sb.Append("\t}\n\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newline.Trim().StartsWith("//"))
|
||||
{
|
||||
sb.AppendLine(newline);
|
||||
continue;
|
||||
}
|
||||
|
||||
int index = newline.IndexOf(";");
|
||||
newline = newline.Remove(index);
|
||||
sb.Append($"\t\t{newline},\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("}\n");
|
||||
using (FileStream txt = new FileStream(csPath, FileMode.Create, FileAccess.ReadWrite))
|
||||
{
|
||||
using (StreamWriter sw = new StreamWriter(txt))
|
||||
{
|
||||
Debug.Log(sb.ToString());
|
||||
sw.Write(sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Repeated(StringBuilder sb, string ns, string newline)
|
||||
{
|
||||
try
|
||||
{
|
||||
int index = newline.IndexOf(";");
|
||||
newline = newline.Remove(index);
|
||||
string[] ss = newline.Split(splitChars, StringSplitOptions.RemoveEmptyEntries);
|
||||
string type = ss[1];
|
||||
type = ConvertType(type);
|
||||
string name = ss[2];
|
||||
int n = int.Parse(ss[4]);
|
||||
|
||||
sb.Append($"\t\t[global::ProtoBuf.ProtoMember({n})]\n");
|
||||
sb.Append($"\t\tpublic List<{type}> {name} = new List<{type}>();\n\n");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"{newline}\n {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertType(string type)
|
||||
{
|
||||
string typeCs = "";
|
||||
switch (type)
|
||||
{
|
||||
case "int16":
|
||||
typeCs = "short";
|
||||
break;
|
||||
case "int32":
|
||||
typeCs = "int";
|
||||
break;
|
||||
case "bytes":
|
||||
typeCs = "byte[]";
|
||||
break;
|
||||
case "uint32":
|
||||
typeCs = "uint";
|
||||
break;
|
||||
case "long":
|
||||
typeCs = "long";
|
||||
break;
|
||||
case "int64":
|
||||
typeCs = "long";
|
||||
break;
|
||||
case "uint64":
|
||||
typeCs = "ulong";
|
||||
break;
|
||||
case "uint16":
|
||||
typeCs = "ushort";
|
||||
break;
|
||||
default:
|
||||
typeCs = type;
|
||||
break;
|
||||
}
|
||||
|
||||
return typeCs;
|
||||
}
|
||||
|
||||
private static void Members(StringBuilder sb, string newline, bool isRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
int index = newline.IndexOf(";");
|
||||
newline = newline.Remove(index);
|
||||
string[] ss = newline.Split(splitChars, StringSplitOptions.RemoveEmptyEntries);
|
||||
string type = ss[0];
|
||||
string name = ss[1];
|
||||
int n = int.Parse(ss[3]);
|
||||
string typeCs = ConvertType(type);
|
||||
|
||||
sb.Append($"\t\t[global::ProtoBuf.ProtoMember({n})]\n");
|
||||
if (string.Equals(type,"string"))
|
||||
{
|
||||
sb.Append($"\t\t[global::System.ComponentModel.DefaultValue(\"\")]\n");
|
||||
}
|
||||
sb.Append($"\t\tpublic {typeCs} {name} {{ get; set; }}\n\n");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"{newline}\n {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/TEngine/Editor/ProtoGenTools/ProtoGenTools.cs.meta
Normal file
11
Assets/TEngine/Editor/ProtoGenTools/ProtoGenTools.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e505998676d79fb47bc6b4ed97148fa1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -3,7 +3,8 @@
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"TEngine.Runtime",
|
||||
"YooAsset.Editor"
|
||||
"YooAsset.Editor",
|
||||
"HybridCLR.Editor"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
|
@@ -27,6 +27,7 @@ MonoBehaviour:
|
||||
m_FormalResourceSourceUrl: http://127.0.0.1:8088
|
||||
m_AtlasFolder: Assets/AssetRaw/UIRaw
|
||||
m_ResourceVersionFileName: ResourceVersion.txt
|
||||
HostServerURL: http://127.0.0.1:8081
|
||||
CheckVersionUrl: http://127.0.0.1/Resources/{0}Version.txt
|
||||
WindowsAppUrl: http://127.0.0.1
|
||||
MacOSAppUrl: http://127.0.0.1
|
||||
@@ -39,9 +40,16 @@ MonoBehaviour:
|
||||
m_BybridCLRCustomGlobalSettings:
|
||||
m_Enable: 1
|
||||
m_Gitee: 1
|
||||
HotUpdateAssemblies: []
|
||||
AOTMetaAssemblies: []
|
||||
LogicMainDllName: xxx.dll
|
||||
HotUpdateAssemblies:
|
||||
- GameLogic.dll
|
||||
- GameBase.dll
|
||||
- GameProto.dll
|
||||
- BattleCore.Runtime.dll
|
||||
AOTMetaAssemblies:
|
||||
- mscorlib.dll
|
||||
- System.dll
|
||||
- System.Core.dll
|
||||
LogicMainDllName: GameLogic.dll
|
||||
AssemblyTextAssetExtension: .bytes
|
||||
AssemblyTextAssetPath: Assets/AssetRaw/DLL
|
||||
AssemblyTextAssetPath: AssetRaw/DLL
|
||||
HybridCLRGlobalSettings: Settings/HybridCLRGlobalSettings
|
||||
|
@@ -28,15 +28,16 @@ public class HybridCLRCustomGlobalSettings
|
||||
set { m_Gitee = value; }
|
||||
}
|
||||
|
||||
[Header("Auto sync with [HybridCLRGlobalSettings]")] [Tooltip("You should modify the file form file path [Assets/CustomHybridCLR/Settings/HybridCLRGlobalSettings.asset]")]
|
||||
public List<string> HotUpdateAssemblies;
|
||||
[Header("Auto sync with [HybridCLRGlobalSettings]")]
|
||||
[Tooltip("You should modify the file form file path [Assets/CustomHybridCLR/Settings/HybridCLRGlobalSettings.asset]")]
|
||||
public List<string> HotUpdateAssemblies = new List<string>() { "GameLogic.dll","GameBase.dll","GameProto.dll","BattleCore.Runtime.dll"};
|
||||
|
||||
[Header("Need manual setting!")] public List<string> AOTMetaAssemblies;
|
||||
[Header("Need manual setting!")] public List<string> AOTMetaAssemblies= new List<string>() {"mscorlib.dll","System.dll","System.Core.dll" };
|
||||
|
||||
/// <summary>
|
||||
/// Dll of main business logic assembly
|
||||
/// </summary>
|
||||
public string LogicMainDllName = "xxx.dll";
|
||||
public string LogicMainDllName = "GameLogic.dll";
|
||||
|
||||
/// <summary>
|
||||
/// 程序集文本资产打包Asset后缀名
|
||||
@@ -46,7 +47,7 @@ public class HybridCLRCustomGlobalSettings
|
||||
/// <summary>
|
||||
/// 程序集文本资产资源目录
|
||||
/// </summary>
|
||||
public string AssemblyTextAssetPath = "Assets/AssetRaw/DLL";
|
||||
public string AssemblyTextAssetPath = "AssetRaw/DLL";
|
||||
|
||||
/// <summary>
|
||||
/// Resources HybridCLRGlobalSettings Dir
|
||||
|
@@ -682,7 +682,9 @@ public sealed class BuglyAgent
|
||||
|
||||
#if UNITY_ANDROID
|
||||
// The crash reporter package name, default is 'com.tencent.bugly'
|
||||
#pragma warning disable CS0414
|
||||
private static string _crashReporterPackage = "com.tencent.bugly";
|
||||
#pragma warning restore CS0414
|
||||
#endif
|
||||
#if UNITY_IPHONE || UNITY_IOS
|
||||
private static int _crashReproterCustomizedLogLevel = 2; // Off=0,Error=1,Warn=2,Info=3,Debug=4
|
||||
|
Reference in New Issue
Block a user