Compare commits

...

3 Commits

Author SHA1 Message Date
afe095286b fix: the unity serializer is well regarded 2024-05-22 06:33:16 +09:00
e7c0dad0d7 fix: epic trailing slash fail 2024-05-20 21:54:06 +00:00
9c180657b4 feat: PB exports 2024-05-21 02:25:56 +09:00
13 changed files with 255 additions and 121 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.dll
*.csproj
bin/
obj/
obj/
dist/

47
Inohara.DT/BatchManual.cs Normal file
View File

@ -0,0 +1,47 @@
using System;
namespace Inohara.DT;
[Serializable]
public struct BatchManual {
public BatchManualMeta meta;
public BatchManualScore[] scores;
}
[Serializable]
public struct BatchManualMeta {
public string game;
public string playtype;
public string service;
}
[Serializable]
public struct BatchManualScore {
public int score;
public string difficulty;
public UInt64 timeAchieved;
public string noteLamp;
public string bellLamp;
public string matchType;
public string identifier;
public BatchManualJudgements judgements;
public BatchManualOptional optional;
}
[Serializable]
public struct BatchManualJudgements {
public int cbreak;
public int breakMyBonesIWill;
public int hit;
public int miss;
}
[Serializable]
public struct BatchManualOptional {
public int fast;
public int slow;
public int bellCount;
public int totalBellCount;
public int damage;
public int platScore;
}

View File

@ -0,0 +1,34 @@
using System;
namespace Inohara.DT;
[Serializable]
public struct BatchManualPB {
public BatchManualPBMeta meta;
public BatchManualPBScore[] scores;
}
[Serializable]
public struct BatchManualPBMeta {
public string game;
public string playtype;
public string service;
}
[Serializable]
public struct BatchManualPBScore {
public int score;
public string difficulty;
public string noteLamp;
public string bellLamp;
public string matchType;
public string identifier;
public BatchManualPBJudgements judgements;
public BatchManualPBOptional optional;
}
[Serializable]
public struct BatchManualPBJudgements {}
[Serializable]
public struct BatchManualPBOptional {}

View File

@ -0,0 +1,19 @@
using System;
namespace Inohara.DT;
[Serializable]
public struct BatchResponse {
public bool success;
public BatchResponseBody body;
}
[Serializable]
public struct BatchResponseBody {
public string url;
}
[Serializable]
public struct BatchResponse2 {
public bool success;
}

View File

@ -0,0 +1,16 @@
using System;
namespace Inohara.DT;
[Serializable]
public struct StatusResponse {
public bool success;
public StatusBody body;
}
[Serializable]
public struct StatusBody {
public string version;
public string whoami;
public string[] permissions;
}

View File

@ -16,4 +16,10 @@
<Reference Include="UnityEngine.UI"><HintPath></HintPath></Reference>
<Reference Include="Mu3Assembly"><HintPath></HintPath></Reference>
</ItemGroup>
<Target Name="Dist" AfterTargets="Build">
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="dist" />
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="dist/BepInEx/monomod" />
<ZipDirectory SourceDirectory="dist/BepInEx" DestinationFile="dist/inohara.zip" Overwrite="true" />
</Target>
</Project>

View File

@ -1,92 +0,0 @@
using System;
namespace Inohara;
/**
* Batch manual
*/
[Serializable]
public struct BatchManual {
public BatchMeta meta;
public BatchScore[] scores;
}
[Serializable]
public struct BatchMeta {
public string game;
public string playtype;
public string service;
}
[Serializable]
public struct BatchScore {
public int score;
public string difficulty;
public UInt64 timeAchieved;
public string noteLamp;
public string bellLamp;
public string matchType;
public string identifier;
public BatchJudgements judgements;
public BatchOptional optional;
}
[Serializable]
public struct BatchJudgements {
public int cbreak;
public int breakMyBonesIWill;
public int hit;
public int miss;
}
[Serializable]
public struct BatchOptional {
public int fast;
public int slow;
public int bellCount;
public int totalBellCount;
public int damage;
public int platScore;
}
/**
* BM response 1
*/
[Serializable]
public struct BatchResponse {
public bool success;
public BatchResponseBody body;
}
[Serializable]
public struct BatchResponseBody {
public string url;
}
/**
* BM response 2
*/
[Serializable]
public struct BatchResponse2 {
public bool success;
}
/**
* API status response
*/
[Serializable]
public struct StatusResponse {
public bool success;
public StatusBody body;
}
[Serializable]
public struct StatusBody {
public string version;
public string whoami;
public string[] permissions;
}

View File

@ -11,6 +11,7 @@ using MU3.Util;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Inohara.DT;
namespace Inohara;
@ -22,6 +23,7 @@ public class Exporter: SingletonMonoBehaviour<Exporter> {
public string StatusPoint;
public string ImportPoint;
public bool EnableText;
public bool ExportPBs;
}
static readonly float REQ2_DELAY = 4f;
static readonly float DRAW_DURATION = 4f;
@ -30,11 +32,11 @@ public class Exporter: SingletonMonoBehaviour<Exporter> {
private readonly Dictionary<string, string> _tokens = new();
private string _currToken = "";
private readonly Font _arial = Resources.GetBuiltinResource<Font>("Arial.ttf");
private List<BatchScore> _scores = new();
private List<BatchManualScore> _scores = new();
private void Log(object o, bool text) {
private void Log(object o, bool drawText) {
Debug.Log("[Inohara] " + o.ToString());
if(_cfg.EnableText && text) {
if(_cfg.EnableText && drawText) {
StartCoroutine(DrawMessage(o.ToString(), new Color(1f, 1f, 1f, 1.0f)));
}
}
@ -54,6 +56,7 @@ public class Exporter: SingletonMonoBehaviour<Exporter> {
}
public void LoadCfg() {
_cfg.Enable = false;
try {
using StreamReader reader = new(Path.Combine(Application.dataPath, "../inohara.cfg"));
Dictionary<string, string> options = new();
@ -81,6 +84,9 @@ public class Exporter: SingletonMonoBehaviour<Exporter> {
if(!options.ContainsKey("enableosd") || !bool.TryParse(options["enableosd"], out _cfg.EnableText)) {
_cfg.EnableText = false;
}
if(!options.ContainsKey("exportpbs") || !bool.TryParse(options["exportpbs"], out _cfg.ExportPBs)) {
_cfg.ExportPBs = false;
}
if(!options.ContainsKey("timeout") || !int.TryParse(options["timeout"], out _cfg.Timeout)) {
_cfg.Timeout = 3;
}
@ -136,6 +142,7 @@ public class Exporter: SingletonMonoBehaviour<Exporter> {
using var req = new UnityWebRequest(_cfg.BaseUrl + _cfg.StatusPoint, "GET");
req.SetRequestHeader("Authorization", "Bearer " + tmpBearer);
req.downloadHandler = new DownloadHandlerBuffer();
req.timeout = _cfg.Timeout;
yield return req.Send();
@ -221,6 +228,27 @@ public class Exporter: SingletonMonoBehaviour<Exporter> {
}
}
public void ExportPBs() {
if(!_cfg.ExportPBs) {
return;
}
var userMusic = Singleton<UserManager>.instance.userMusic;
var scores = new List<BatchManualPBScore>();
foreach(var x in userMusic.Values) {
foreach(var y in x.UserFumen) {
if(y != null) {
scores.Add(Util.CreatePB(y));
}
}
}
var batch = Util.CreatePBBatch(scores);
using StreamWriter writer = new(Path.Combine(Application.dataPath, "../batch-manual.json"));
writer.Write(batch);
Log("Exported PBs to batch-manual.json", false);
}
// This is just for fun
private IEnumerator DrawMessage(string message, Color color) {
GameObject canvasGO = new() {

View File

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using Inohara.DT;
using MU3.Battle;
using MU3.DataStudio;
using MU3.Game;
using MU3.User;
using UnityEngine;
namespace Inohara;
@ -35,9 +37,9 @@ class Util {
};
}
public static BatchScore CreateScore(BattleResult result, SessionInfo info) {
public static BatchManualScore CreateScore(BattleResult result, SessionInfo info) {
var timestampSec = (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
return new BatchScore {
return new BatchManualScore {
score = result.technicalScore,
difficulty = GetStringDiff(info.musicLevel),
timeAchieved = (ulong)timestampSec * 1000,
@ -45,7 +47,7 @@ class Util {
identifier = info.musicData.id.ToString(),
bellLamp = result.bellComboResult == BellComboResult.None ? "NONE" : "FULL BELL",
noteLamp = GetLamp(result),
optional = new BatchOptional() {
optional = new BatchManualOptional() {
fast = result.numNotesFast,
slow = result.numNotesLate,
bellCount = result.numBellCatch,
@ -53,7 +55,7 @@ class Util {
damage = result.countDamage,
platScore = result.platinumScore
},
judgements = new BatchJudgements() {
judgements = new BatchManualJudgements() {
cbreak = result.numNotesCBreak,
breakMyBonesIWill = result.numNotesBreak,
hit = result.numNotesHit,
@ -62,9 +64,28 @@ class Util {
};
}
public static string CreateBatch(List<BatchScore> scores) {
public static BatchManualPBScore CreatePB(UserFumen fumen) {
return new BatchManualPBScore {
score = fumen.TechScoreMax,
difficulty = GetStringDiff(fumen.Level),
matchType = "inGameID",
identifier = fumen.MusicId.ToString(),
bellLamp = fumen.IsFullBell ? "FULL BELL" : "NONE",
noteLamp =
fumen.IsAllBreak ? "ALL BREAK" :
fumen.IsFullCombo ? "FULL COMBO" :
// fumen.isClear is a bool that seems to flag battle victory. Useless
// basing this on score is ass but for the most part it will be accurate.
fumen.TechScoreMax >= 940000 ? "CLEAR" :
"LOSS",
optional = new BatchManualPBOptional() {},
judgements = new BatchManualPBJudgements() {}
};
}
public static string CreateBatch(List<BatchManualScore> scores) {
var bm = new BatchManual {
meta = new BatchMeta {
meta = new BatchManualMeta {
game = "ongeki",
playtype = "Single",
service = "inohara"
@ -74,4 +95,17 @@ class Util {
return JsonUtility.ToJson(bm).Replace("breakMyBonesIWill", "break");
}
public static string CreatePBBatch(List<BatchManualPBScore> scores) {
var bm = new BatchManualPB {
meta = new BatchManualPBMeta {
game = "ongeki",
playtype = "Single",
service = "inohara-pb"
},
scores = scores.ToArray()
};
return JsonUtility.ToJson(bm);
}
}

View File

@ -9,16 +9,16 @@ using MU3.Util;
namespace MU3.App;
public class patch_ApplicationMU3 : ApplicationMU3 {
private extern void orig_Execute_WaitAMDaemonReady();
private extern void orig_initializeAfterAMDaemonReady();
private void Execute_WaitAMDaemonReady() {
private void initializeAfterAMDaemonReady() {
GameObject go = new() {
name = "Inohara"
};
go.AddComponent<Inohara.Exporter>();
DontDestroyOnLoad(go);
orig_Execute_WaitAMDaemonReady();
orig_initializeAfterAMDaemonReady();
SingletonMonoBehaviour<Inohara.Exporter>.instance.LoadCfg();
}
}

View File

@ -10,9 +10,15 @@ namespace MU3;
public class patch_Scene_25_Login : Scene_25_Login {
private extern void orig_finishAime();
private extern void orig_GetUserRival_Init();
private void finishAime() {
orig_finishAime();
SingletonMonoBehaviour<Inohara.Exporter>.instance.Authorize();
}
private void GetUserRival_Init() {
orig_GetUserRival_Init();
SingletonMonoBehaviour<Inohara.Exporter>.instance.ExportPBs();
}
}

View File

@ -8,31 +8,55 @@ A µ3 score exporter for [Tachi](https://github.com/zkldi/Tachi).
- 1.45
### Installation
Get the config file [here](https://kamai.tachi.ac/client-file-flow/CIa914320cd344a8db712cf0c99254c205ca940463), download the DLL [here](https://gitea.tendokyu.moe/akanyan/inohara/releases) and follow one of the methods below.
First, get the config file [here](https://kamai.tachi.ac/client-file-flow/CIa914320cd344a8db712cf0c99254c205ca940463) and put it in the base game directory (next to `mu3.exe`), then follow one of the methods below.
#### The BepInEx method (recommended)
- Download [BepInEx](https://github.com/BepInEx/BepInEx/releases/)
- Copy the `BepInEx` directory into the base game directory (where `mu3.exe` is); omit `winhttp.dll`
#### The simple method
- Update [segatools](https://gitea.tendokyu.moe/Dniel97/segatools).
- Download `inohara.zip` from [releases](https://gitea.tendokyu.moe/akanyan/inohara/releases) and extract it into the base game directory.
- Modify this entry in `segatools.ini`:
```ini
[unity]
targetAssembly=BepInEx\core\BepInEx.Preloader.dll
```
- The game directory should look like this (abridged):
```
├── BepInEx
├── mu3_Data
├── inohara.cfg
├── mu3.exe
└── segatools.ini
```
#### The manual BepInEx method
- Download [BepInEx 5](https://github.com/BepInEx/BepInEx/releases/) and [BepInEx.MonoMod.Loader](https://github.com/BepInEx/BepInEx.MonoMod.Loader).
- Copy both `BepInEx` directories into the base game directory; omit `winhttp.dll`.
- Modify this entry in `segatools.ini`:
```ini
[unity]
targetAssembly=BepInEx\core\BepInEx.Preloader.dll
```
- If you don't have this entry, update segatools.
- If you insist on not updating segatools, instead copy `winhttp.dll` and rename it to `version.dll`.
- Move `Assembly-CSharp.Inohara.mm.dll` to `BepInEx\monomod`
- Put `inohara.cfg` in the base game directory (next to `mu3.exe`)
- Download `Assembly-CSharp.Inohara.mm.dll` from [releases](https://gitea.tendokyu.moe/akanyan/inohara/releases) and put it in `BepInEx\monomod`.
#### The MonoMod method
- Download [MonoMod](https://github.com/MonoMod/MonoMod/releases)
- Copy `Assembly-CSharp.Inohara.mm.dll` into `mu3_Data\Managed`
- Run `MonoMod.exe mu3_Data\Managed\Assembly-CSharp.dll`
- Backup `Assembly-CSharp.dll`
- Rename `MONOMODDED_Assembly-CSharp.dll` to `Assembly-CSharp.dll`
- Move `inohara.cfg` to the base game directory (next to `mu3.exe`)
#### The hardpatch method
- Download [MonoMod](https://github.com/MonoMod/MonoMod/releases).
- Download `Assembly-CSharp.Inohara.mm.dll` from [releases](https://gitea.tendokyu.moe/akanyan/inohara/releases) and put it in `mu3_Data\Managed`.
- Run:
```
MonoMod.exe mu3_Data\Managed\Assembly-CSharp.dll
```
- Backup `Assembly-CSharp.dll`.
- Rename `MONOMODDED_Assembly-CSharp.dll` to `Assembly-CSharp.dll`.
### Usage
Scores are sent after each play and that's it. You can nonetheless make sure it's running by checking the console or toggling `EnableOSD`.
Scores are sent after each play and that's it. You can nonetheless make sure it's running by enabling the console in `BepInEx\config\BepInEx.cfg` or the OSD in `inohara.cfg`.
#### Uploading older scores
Use [this script](https://gist.github.com/nyairobi/ffdf9e674f31987b1ffbd38d31b55f6c). You only have to do this once as Inohara will handle all future scores. If you are on a remote server, contact the admin.
**As a last resort**, you can toggle `ExportPBs` and upload the generated `batch-manual.json` [here](https://kamai.tachi.ac/import/batch-manual). Those PBs are industrial grade dogshit. Should the server admin provide you access to the playlog down the line, delete the `inohara-pb` import.
### Building
Provide your own `Assembly-CSharp.dll` (or `_unpacked`) and `UnityEngine.UI.dll`, then `dotnet restore`, `dotnet build`.

View File

@ -1,18 +1,29 @@
[Options]
# Whether to enable score submissions
Enable = true
# Timeout for web requests, in seconds
Timeout = 3
# Tachi instance base URL
BaseUrl =
BaseUrl = https://kamai.tachi.ac
# Tachi status endpoint
Status = /api/v1/status
# Tachi score import endpoint
Import = /ir/direct-manual/import
# Display status on-screen (rudimentarily)
# Whether to display status on-screen (rudimentarily)
EnableOSD = false
# Whether to export your PBs for batch-manual (saved to batch-manual.json)
# This is very, very bad and should only be used as last resort
ExportPBs = false
[Keys]
* = %%TACHI_KEY%%
# If you have a multi-user setup, you can configure