add main logic

This commit is contained in:
Beu
2024-12-18 16:57:45 +01:00
parent e0f72cc6ce
commit ab3463fa16
6 changed files with 378 additions and 137 deletions

View File

@@ -0,0 +1,37 @@
class InterfacesConfig {
string Name;
string Author;
string Version;
string ModePattern;
array<LayerConfig> Layers;
array<string> UnloadModules;
InterfacesConfig() {}
InterfacesConfig(const string &in _ParentPath, const string &in _Json) {
const Json::Value@ Data = Json::Parse(_Json);
Name = Data['Name'];
Author = Data['Author'];
Version = Data['Version'];
ModePattern = Data['ModePattern'];
if (Data.HasKey('Layers') && Data['Layers'].GetType() == Json::Type::Array) {
for (uint i = 0; i < Data['Layers'].Length; i++) {
try {
const LayerConfig LayerConfig(_ParentPath, Data['Layers'][i]);
Layers.InsertLast(LayerConfig);
} catch {
warn("Can't load layer \""+ i +"\" of Interface Config: \""+ Name +"\"");
}
}
}
if (Data.HasKey('UnloadModules') && Data['UnloadModules'].GetType() == Json::Type::Array) {
for (uint i = 0; i < Data['UnloadModules'].Length; i++) {
UnloadModules.InsertLast(Data['UnloadModules'][i]);
}
}
}
}

51
Classes/LayerConfig.as Normal file
View File

@@ -0,0 +1,51 @@
class LayerConfig {
string Name;
string Path;
CGameUILayer::EUILayerType LayerType = CGameUILayer::EUILayerType::Normal;
string AttachId = '';
LayerConfig() {}
LayerConfig(const string &in _ParentPath, const Json::Value &in _Data) {
Path = Path::Join(_ParentPath, _Data['Path']);
const string Manialink = GetManialink();
Name = InterfacesManager::GetLayerName(Manialink);
if (_Data.HasKey('LayerType') && _Data['LayerType'].GetType() == Json::Type::String) {
LayerType = CastEUILayerType(_Data['LayerType']);
}
if (_Data.HasKey('AttachId') && _Data['AttachId'].GetType() == Json::Type::String) {
AttachId = _Data['AttachId'];
}
}
string GetManialink() const {
if (!IO::FileExists(Path)) {
throw('Invalid Layer: Can\'t find "' + Path + '"');
}
IO::File File(Path, IO::FileMode::Read);
const string Manialink = File.ReadToEnd();
return Manialink;
}
CGameUILayer::EUILayerType CastEUILayerType(const string &in _LayerType) {
if (_LayerType == 'Normal') return CGameUILayer::EUILayerType::Normal;
if (_LayerType == 'ScoresTable') return CGameUILayer::EUILayerType::ScoresTable;
if (_LayerType == 'ScreenIn3d') return CGameUILayer::EUILayerType::ScreenIn3d;
if (_LayerType == 'AltMenu') return CGameUILayer::EUILayerType::AltMenu;
if (_LayerType == 'Markers') return CGameUILayer::EUILayerType::Markers;
if (_LayerType == 'CutScene') return CGameUILayer::EUILayerType::CutScene;
if (_LayerType == 'InGameMenu') return CGameUILayer::EUILayerType::InGameMenu;
if (_LayerType == 'EditorPlugin') return CGameUILayer::EUILayerType::EditorPlugin;
if (_LayerType == 'ManiaplanetPlugin') return CGameUILayer::EUILayerType::ManiaplanetPlugin;
if (_LayerType == 'ManiaplanetMenu') return CGameUILayer::EUILayerType::ManiaplanetMenu;
if (_LayerType == 'LoadingScreen') return CGameUILayer::EUILayerType::LoadingScreen;
throw('Invalid EUILayerType: '+ _LayerType);
return CGameUILayer::EUILayerType::Normal;
}
}

210
InterfacesManager.as Normal file
View File

@@ -0,0 +1,210 @@
namespace InterfacesManager {
uint G_UILayersCount;
dictionary G_Configs;
array<string> G_EnabledConfigs;
void LoadConfigs() {
G_Configs = {};
const string Root = IO::FromStorageFolder('');
const array<string>@ Folders = IO::IndexFolder(Root, false);
for (uint i = 0; i < Folders.Length; i++) {
const string Path = Folders[i];
// Get Id
string Id;
const array<string> Parts = Path.Split('/');
for (uint j = Parts.Length - 1; j >= 0; j--) {
if (Parts[j].Length > 0) {
Id = Parts[j];
break;
}
}
if (IO::FileExists(Path)) {
trace('Invalid Interface Config: "' + Path + '" is a file');
continue;
}
const string InfoFile = Path::Join(Path, 'info.json');
if (!IO::FileExists(InfoFile)) {
trace('Invalid Interface Config: "' + Path + '" has no info.json file');
continue;
}
IO::File File(InfoFile, IO::FileMode::Read);
if (File.Size() <= 0) {
trace('Invalid Interface Config: "' + Path + '" is not readable');
continue;
}
const string FileContent = File.ReadToEnd();
try {
const InterfacesConfig Config(Path, FileContent);
G_Configs[Id] = Config;
} catch {
warn('Invalid Interface Config: "' + Path + '" is an invalid json');
}
}
if (S_EnabledConfigs.Length > 0) {
G_EnabledConfigs = S_EnabledConfigs.Split(',');
}
for (uint i = 0; i < G_EnabledConfigs.Length; i++) {
const string Id = G_EnabledConfigs[i];
if (G_Configs.Exists(Id)) {
continue;
}
RenderManager::NotifyWarning("Enabled interfaces " + Id + " has been deleted.");
G_EnabledConfigs.RemoveAt(i);
}
}
void InjectInterfacesIfNeeded() {
CTrackManiaNetwork@ Network = cast<CTrackManiaNetwork>(GetApp().Network);
CGameManiaAppPlayground@ ManiaApp = Network.ClientManiaAppPlayground;
CTrackManiaNetworkServerInfo@ ServerInfo = cast<CTrackManiaNetworkServerInfo>(Network.ServerInfo);
if (ManiaApp !is null) {
MwFastBuffer<CGameUILayer@> UILayers = ManiaApp.UILayers;
dictionary IndexedLayers = GetIndexedUILayers(UILayers);
for (uint i = 0; i < G_EnabledConfigs.Length; i++) {
const string Id = G_EnabledConfigs[i];
const InterfacesConfig@ Config = InterfacesManager::GetConfig(Id);
// TODO Check ModePattern
if (Regex::IsMatch(ServerInfo.CurScriptRelName, Config.ModePattern)) {
print('match');
}
for (uint j = 0; j < Config.UnloadModules.Length; j++) {
const string ModuleId = Config.UnloadModules[j];
if (IndexedLayers.Exists(ModuleId)) {
print('Removing UI Layer: "' + ModuleId + '"');
CGameUILayer@ Layer = cast<CGameUILayer>(IndexedLayers[ModuleId]);
/**
* Some UILayer can have Maniascript that can interact with the game, even hidden.
* So it's safer to destroy them.
*/
ManiaApp.UILayerDestroy(Layer);
}
}
for (uint j = 0; j < Config.Layers.Length; j++) {
const LayerConfig@ LayerConfig = Config.Layers[j];
CGameUILayer@ Layer;
if (IndexedLayers.Exists(LayerConfig.Name)) {
print('Updating Layer "'+ LayerConfig.Name +'"');
@Layer = cast<CGameUILayer>(IndexedLayers[LayerConfig.Name]);
} else {
print('Creating new Layer "'+ LayerConfig.Name +'"');
@Layer = ManiaApp.UILayerCreate();
}
const string Manialink = LayerConfig.GetManialink();
Layer.ManialinkPage = Manialink;
Layer.AttachId = LayerConfig.AttachId;
Layer.Type = LayerConfig.LayerType;
}
}
G_UILayersCount = UILayers.Length;
}
}
void EnableInterface(const string &in _Id) {
trace('Enabling Interface: '+ _Id);
const int Index = G_EnabledConfigs.Find(_Id);
if (Index >= 0) {
trace("Can't disable config \""+ _Id +"\": already enabled");
return;
}
G_EnabledConfigs.InsertLast(_Id);
S_EnabledConfigs = string::Join(G_EnabledConfigs, ',');
InjectInterfacesIfNeeded();
}
void DisableInterface(const string &in _Id) {
trace('Disabling Interface: '+ _Id);
const int Index = G_EnabledConfigs.Find(_Id);
if (Index < 0) {
trace("Can't disable config \""+ _Id +"\": not enabled");
return;
}
G_EnabledConfigs.RemoveAt(Index);
S_EnabledConfigs = string::Join(G_EnabledConfigs, ',');
}
InterfacesConfig@ GetConfig(const string &in _Id) {
if (InterfacesManager::G_Configs.Exists(_Id)) {
return cast<InterfacesConfig>(InterfacesManager::G_Configs[_Id]);
}
return null;
}
dictionary GetIndexedUILayers(MwFastBuffer<CGameUILayer@> _UILayers) {
dictionary IndexedLayers;
for (uint i = 0; i < _UILayers.Length; i++) {
CGameUILayer@ UILayer = _UILayers[i];
const string Name = GetLayerName(UILayer.ManialinkPage);
if (Name.Length > 0) {
IndexedLayers[Name] = @UILayer;
}
}
return IndexedLayers;
}
string GetLayerName(const string &in _Manialink) {
array<string> FirstLines = _Manialink.Split("\n", 5);
if (FirstLines.Length > 0) {
for (uint j = 0; j < FirstLines.Length - 1; j++) {
if (FirstLines[j].Contains('<manialink ')) {
string[] match = Regex::Search(FirstLines[j], 'name="(\\w*)"');
if (match.Length == 2) {
return match[1];
}
}
}
}
return '';
}
void Yield() {
CTrackManiaNetwork@ Network = cast<CTrackManiaNetwork>(GetApp().Network);
if (Network.ClientManiaAppPlayground !is null) {
CGameManiaAppPlayground @ManiaApp = Network.ClientManiaAppPlayground;
MwFastBuffer<CGameUILayer@> UILayers = ManiaApp.UILayers;
/**
* Gamemode can inject UILayer at any time
* IMO, it's better to check count instead of gamemode name or anything else
*/
if (G_UILayersCount != UILayers.Length) {
G_UILayersCount = UILayers.Length;
InjectInterfacesIfNeeded();
}
} else {
G_UILayersCount = 0;
}
}
}

70
RenderManager.as Normal file
View File

@@ -0,0 +1,70 @@
namespace RenderManager {
bool G_RenderInterface = Meta::IsDeveloperMode(); // render by default in dev mode
void RenderInterface() {
if (G_RenderInterface) {
UI::Begin(Icons::Television + " Custom Interface Loader", G_RenderInterface);
if (UI::Button('Open Folder')) {
OpenExplorerPath(IO::FromStorageFolder(''));
}
UI::SameLine();
if (UI::Button('Reload')) {
InterfacesManager::LoadConfigs();
}
if (UI::BeginTable('configs', 5, UI::TableFlags(UI::TableFlags::Resizable | UI::TableFlags::Sortable | UI::TableFlags::NoSavedSettings | UI::TableFlags::BordersInnerV | UI::TableFlags::SizingStretchProp | UI::TableFlags::ScrollY))) {
UI::TableSetupColumn("Name", UI::TableColumnFlags::DefaultSort);
UI::TableSetupColumn("Author", UI::TableColumnFlags::None);
UI::TableSetupColumn("Version", UI::TableColumnFlags::None);
UI::TableSetupColumn("Mode Pattern", UI::TableColumnFlags::NoResize);
UI::TableSetupColumn("Enable", UI::TableFlags(UI::TableColumnFlags::NoResize | UI::TableColumnFlags::WidthFixed), 60.);
UI::TableHeadersRow();
const array<string> ConfigIds = InterfacesManager::G_Configs.GetKeys();
for (uint i = 0; i < ConfigIds.Length; i++) {
const string Id = ConfigIds[i];
const InterfacesConfig@ Config = InterfacesManager::GetConfig(Id);
UI::TableNextRow();
UI::TableNextColumn();
UI::Text(Config.Name);
UI::TableNextColumn();
UI::Text(Config.Author);
UI::TableNextColumn();
UI::Text(Config.Version);
UI::TableNextColumn();
UI::Text(Config.ModePattern);
UI::TableNextColumn();
const int index = InterfacesManager::G_EnabledConfigs.Find(Id);
const bool enabled = (index >= 0);
const bool newValue = UI::Checkbox('###'+ Id, enabled);
if (newValue != enabled) {
if (enabled) {
InterfacesManager::DisableInterface(Id);
} else {
InterfacesManager::EnableInterface(Id);
}
}
}
UI::EndTable();
}
UI::End();
}
}
void RenderMenu() {
if (UI::MenuItem(Icons::Television + ' Custom Interface Loader', "", G_RenderInterface)) {
G_RenderInterface = !G_RenderInterface;
}
}
void NotifyWarning(const string &in _Message) {
UI::ShowNotification('Custom Interfaces Loader', _Message, vec4(0.9, 0.7, 0.2, 1.), 5000);
}
}

3
Settings.as Normal file
View File

@@ -0,0 +1,3 @@
[Setting hidden]
string S_EnabledConfigs = '';

144
main.as
View File

@@ -1,144 +1,14 @@
const array<string> C_ModuleToDisable = {
"UIModule_ChampionSpring2022_MatchInfo",
"UIModule_ChampionSpring2022_ScoresHeader",
"UIModule_ChampionSpring2022_Sponsors",
"UIModule_ChampionCup_LiveRanking",
"UIModule_ChampionCup_SpectatorInfo",
"UIModule_ChampionCup_Chat",
"UIModule_ChampionCup_MapInfo",
"UIModule_ChampionCup_Countdown",
"UIModule_Race_ScoresTable",
"UIModule_ChampionCup_Sign16x9_1",
"UIModule_ChampionCup_Sign16x9_2",
"UIModule_ChampionCup_Sign16x9_3",
"UIModule_ChampionCup_Sign16x9_4",
"UIModule_ChampionCup_Sign16x9_5",
"UIModule_ChampionCup_Sign64x10_64x10_Start",
"UIModule_ChampionCup_Sign64x10_64x10_Finish",
"UIModule_ChampionCup_Sign64x10_64x10_Checkpoint",
"UIModule_ChampionCup_Sign2x3_1"
};
void Main() {
auto Network = cast<CTrackManiaNetwork>(GetApp().Network);
InterfacesManager::LoadConfigs();
/**
* Unload already loaded custom UI
*/
if (Network.ClientManiaAppPlayground !is null) {
CGameManiaAppPlayground @ManiaApp = Network.ClientManiaAppPlayground;
array<string> Manialinks = GetManialinkFiles();
array<string> CustomInterfacesIds;
for (uint i = 0; i < Manialinks.Length; i++) {
CustomInterfacesIds.InsertLast(Path::GetFileNameWithoutExtension(Manialinks[i]));
}
MwFastBuffer<CGameUILayer@> UILayers = ManiaApp.UILayers;
for (uint i = 0; i < UILayers.Length; i++) {
CGameUILayer @UILayer = UILayers[i];
string ManialinkPage = UILayer.ManialinkPage;
array<string> FirstLines = ManialinkPage.Split("\n", 5);
if (FirstLines.Length > 0) {
for (uint j = 0; j < FirstLines.Length - 1; j++) {
if (FirstLines[j].Contains('<manialink ')) {
string[] match = Regex::Search(FirstLines[j], 'name="(\\w*)"');
if (match.Length == 2) {
string LayerId = match[1];
if (LayerId.StartsWith('Custom_TMWC2024_')) {
if (CustomInterfacesIds.Find(LayerId) > -1) {
print("Updating existing layer " + LayerId);
UILayer.ManialinkPage = GetManialinkPage(LayerId);
} else {
print("Destroying not existing layer " + LayerId);
ManiaApp.UILayerDestroy(UILayer);
}
}
}
}
}
}
}
while (true) {
yield();
InterfacesManager::Yield();
}
}
uint count = 0;
while(true) {
yield();
if (Network.ClientManiaAppPlayground !is null) {
CGameManiaAppPlayground @ManiaApp = Network.ClientManiaAppPlayground;
MwFastBuffer<CGameUILayer@> UILayers = ManiaApp.UILayers;
if (count != UILayers.Length) {
print('Walking through UILayers');
array<string> CustomUIModulesLoaded = {};
for (uint i = 0; i < UILayers.Length; i++) {
CGameUILayer @UILayer = UILayers[i];
string ManialinkPage = UILayer.ManialinkPage;
array<string> FirstLines = ManialinkPage.Split("\n", 5);
if (FirstLines.Length > 0) {
for (uint j = 0; j < FirstLines.Length - 1; j++) {
if (FirstLines[j].Contains('<manialink ')) {
string[] match = Regex::Search(FirstLines[j], 'name="(\\w*)"');
if (match.Length == 2) {
string id = match[1];
/**
* Delete UI
*/
if (C_ModuleToDisable.Find(id) > -1) {
print('Deleting ' + id);
ManiaApp.UILayerDestroy(UILayers[i]);
}
if (id.StartsWith('Custom_TMWC2024_')) {
CustomUIModulesLoaded.InsertLast(id);
}
}
}
}
}
}
array<string> Manialinks = GetManialinkFiles();
for (uint i = 0; i < Manialinks.Length; i++) {
string FilePath = Manialinks[i];
string FileName = Path::GetFileName(FilePath);
if (FileName.EndsWith('.xml')) {
string LayerId = Path::GetFileNameWithoutExtension(FileName);
if (CustomUIModulesLoaded.Find(LayerId) == -1) {
print("Loading layer " + LayerId);
CGameUILayer @UILayer = ManiaApp.UILayerCreate();
UILayer.ManialinkPage = GetManialinkPage(LayerId);
if (LayerId.Contains("_Sign16x9Small") || LayerId.Contains("_Sign155Small")) {
UILayer.Type = CGameUILayer::EUILayerType::ScreenIn3d;
UILayer.AttachId = "155_StadiumSmall";
} else if (LayerId.Contains("_Sign16x9") || LayerId.Contains("_Sign155")) {
UILayer.Type = CGameUILayer::EUILayerType::ScreenIn3d;
UILayer.AttachId = "155_Stadium";
} else if (LayerId.Contains("_Sign2x3")) {
UILayer.Type = CGameUILayer::EUILayerType::ScreenIn3d;
UILayer.AttachId = "2x3_Stadium";
} else if (LayerId.Contains("_Markers")) {
UILayer.Type = CGameUILayer::EUILayerType::Markers;
}
}
}
}
count = ManiaApp.UILayers.Length;
}
} else {
count = 0;
}
}
void RenderInterface() {
RenderManager::RenderInterface();
}
array<string> GetManialinkFiles() {