static void Install( Apk apk, SerializedAssets assets, HashSet <string> toInstall, InvocationResult res, Dictionary <string, string> levels ) { foreach (string levelID in toInstall) { string levelFolder = levels[levelID]; try { JsonLevel level = JsonLevel.LoadFromFolder(levelFolder); // We use transactions here so if these throw // an exception, which happens when levels are // invalid, then it doesn't modify the APK in // any way that might screw things up later. var assetsTxn = new SerializedAssets.Transaction(assets); var apkTxn = new Apk.Transaction(); AssetPtr levelPtr = level.AddToAssets(assetsTxn, apkTxn, levelID); // Danger should be over, nothing here should fail assetsTxn.ApplyTo(assets); apkTxn.ApplyTo(apk); res.installedLevels.Add(levelID); } catch (FileNotFoundException e) { res.installSkipped.Add(levelID, $"Missing file referenced by level: {e.FileName}"); } catch (JsonReaderException e) { res.installSkipped.Add(levelID, $"Invalid level JSON: {e.Message}"); } } }
static void Main(string[] args) { string jsonString; using (StreamReader reader = new StreamReader(Console.OpenStandardInput(), Console.InputEncoding)) { jsonString = reader.ReadToEnd(); } Invocation inv = JsonConvert.DeserializeObject <Invocation>(jsonString); InvocationResult res = Program.RunInvocation(inv); string jsonOut = JsonConvert.SerializeObject(res, Formatting.None); Console.WriteLine(jsonOut); }
static void Main(string[] args) { while (true) { string jsonString = Console.ReadLine(); Invocation inv = JsonConvert.DeserializeObject <Invocation>(jsonString); InvocationResult res = Program.RunInvocation(inv); string jsonOut = JsonConvert.SerializeObject(res, Formatting.None); Console.WriteLine(jsonOut); if (inv.exitAfterward) { break; } } }
static void UpdateColors(Apk apk, CustomColors colors, InvocationResult res) { SerializedAssets colorAssets = SerializedAssets.FromBytes( apk.ReadEntireEntry(apk.ColorsFile()), apk.version); // There should only be one color manager var colorManager = colorAssets.FindScript <ColorManager>(cm => true); colorManager.UpdateColor(colorAssets, colors.colorA, ColorManager.ColorSide.A); colorManager.UpdateColor(colorAssets, colors.colorB, ColorManager.ColorSide.B); apk.ReplaceAssetsFile(apk.ColorsFile(), colorAssets.ToBytes()); res.newColors = new CustomColors() { colorA = colorManager.colorA.FollowToScript <SimpleColor>(colorAssets), colorB = colorManager.colorB.FollowToScript <SimpleColor>(colorAssets), }; }
static void EnsureInstalled( Apk apk, SerializedAssets assets, HashSet <string> existingLevels, InvocationResult res, Dictionary <string, string> ensureInstalled ) { LevelCollectionBehaviorData extrasCollection = assets.FindExtrasLevelCollection(); foreach (KeyValuePair <string, string> entry in ensureInstalled) { string levelID = entry.Key; string levelFolder = entry.Value; try { JsonLevel level = JsonLevel.LoadFromFolder(levelFolder); if (existingLevels.Contains(levelID)) { res.installSkipped.Add(levelID, "Present"); } else { // We use transactions here so if these throw // an exception, which happens when levels are // invalid, then it doesn't modify the APK in // any way that might screw things up later. var assetsTxn = new SerializedAssets.Transaction(assets); var apkTxn = new Apk.Transaction(); AssetPtr levelPtr = level.AddToAssets(assetsTxn, apkTxn, levelID); // Danger should be over, nothing here should fail assetsTxn.ApplyTo(assets); extrasCollection.levels.Add(levelPtr); apkTxn.ApplyTo(apk); existingLevels.Add(levelID); res.installedLevels.Add(levelID); } } catch (FileNotFoundException e) { res.installSkipped.Add(levelID, $"Missing file referenced by level: {e.FileName}"); } catch (JsonReaderException e) { res.installSkipped.Add(levelID, $"Invalid level JSON: {e.Message}"); } } }
static InvocationResult RunInvocation(Invocation inv) { InvocationResult res = new InvocationResult(); try { using (Apk apk = new Apk(inv.apkPath)) { if (inv.patchSignatureCheck) { apk.PatchSignatureCheck(); res.didSignatureCheckPatch = true; } SerializedAssets mainAssets = SerializedAssets.FromBytes( apk.ReadEntireEntry(apk.MainAssetsFile()), apk.version ); SyncLevels(apk, mainAssets, inv, res); apk.ReplaceAssetsFile(apk.MainAssetsFile(), mainAssets.ToBytes()); if (inv.colors != null) { UpdateColors(apk, inv.colors, res); } if (inv.replaceText != null) { UpdateText(apk, inv.replaceText, res); } apk.Save(); } if (inv.sign) { Signer.Sign(inv.apkPath); res.didSign = true; } } catch (Exception e) { res.error = e.ToString(); } return(res); }
static InvocationResult RunInvocation(Invocation inv) { InvocationResult res = new InvocationResult(); try { using (Apk apk = new Apk(inv.apkPath)) { if (inv.patchSignatureCheck) { apk.PatchSignatureCheck(); res.didSignatureCheckPatch = true; } byte[] data = apk.ReadEntireEntry(apk.MainAssetsFile()); SerializedAssets assets = SerializedAssets.FromBytes(data, apk.version); HashSet <string> existingLevels = assets.ExistingLevelIDs(); if (inv.ensureInstalled.Count > 0) { Program.EnsureInstalled(apk, assets, existingLevels, res, inv.ensureInstalled); byte[] outData = assets.ToBytes(); apk.ReplaceAssetsFile(apk.MainAssetsFile(), outData); } res.presentLevels = existingLevels.ToList(); apk.Save(); } if (inv.sign) { Signer.Sign(inv.apkPath); res.didSign = true; } } catch (Exception e) { res.error = e.ToString(); } return(res); }
static void UpdateText(Apk apk, Dictionary <string, string> replaceText, InvocationResult res) { SerializedAssets textAssets = SerializedAssets.FromBytes(apk.ReadEntireEntry(apk.TextFile()), apk.version); var aotext = textAssets.GetAssetAt(1); TextAssetData ta = aotext.data as TextAssetData; var segments = ta.ReadLocaleText(); TextAssetData.ApplyWatermark(segments); foreach (var entry in replaceText) { Dictionary <string, string> value; if (!segments.TryGetValue(entry.Key, out value)) { continue; } value["ENGLISH"] = entry.Value; } ta.WriteLocaleText(segments); apk.ReplaceAssetsFile(apk.TextFile(), textAssets.ToBytes()); res.didReplaceText = true; }
static void SyncLevels( Apk apk, SerializedAssets mainAssets, Invocation inv, InvocationResult res ) { if (inv.levels == null || inv.packs == null) { throw new ApplicationException("Either the 'levels' or 'packs' key is missing. Note the 'levels' key changed names from 'ensureInstalled' in the new version."); } Dictionary <string, ulong> existingLevels = mainAssets.FindLevels(); ulong maxBasePathID = mainAssets.MainAssetsMaxBaseGamePath(); // === Load root level pack SerializedAssets rootPackAssets = SerializedAssets.FromBytes(apk.ReadEntireEntry(apk.RootPackFile()), apk.version); string mainFileName = apk.MainAssetsFileName(); int mainFileI = rootPackAssets.externals.FindIndex(e => e.pathName == mainFileName) + 1; BeatmapLevelPackCollection rootLevelPack = rootPackAssets.FindMainLevelPackCollection(); // Might be null if we're on a version older than v1.1.0 AlwaysOwnedBehaviorData alwaysOwned = mainAssets.FindScript <AlwaysOwnedBehaviorData>(x => true); // === Remove existing custom packs rootLevelPack.beatmapLevelPacks.RemoveAll(ptr => ptr.fileID == mainFileI && ptr.pathID > maxBasePathID); if (alwaysOwned != null) { alwaysOwned.levelPacks.RemoveAll(ptr => ptr.fileID == 0 && ptr.pathID > maxBasePathID); } LevelPackBehaviorData.RemoveCustomPacksFromEnd(mainAssets); // === Remove old-school custom levels from Extras pack var extrasCollection = mainAssets.FindExtrasLevelCollection(); extrasCollection.levels.RemoveAll(ptr => ptr.pathID > maxBasePathID); // === Remove existing levels var toRemove = new HashSet <string>(); foreach (var entry in existingLevels) { if (inv.levels.ContainsKey(entry.Key)) { continue; // requested } if (entry.Value <= maxBasePathID) { continue; // base game level } toRemove.Add(entry.Key); } foreach (string levelID in toRemove) { var ao = mainAssets.GetAssetObjectFromScript <LevelBehaviorData>(p => p.levelID == levelID); var apkTxn = new Apk.Transaction(); Utils.RemoveLevel(mainAssets, ao, apkTxn); apkTxn.ApplyTo(apk); } res.removedLevels = toRemove.ToList(); // === Install new levels var toInstall = new HashSet <string>(); foreach (var entry in inv.levels) { if (existingLevels.ContainsKey(entry.Key)) { continue; // already installed } toInstall.Add(entry.Key); } Program.Install(apk, mainAssets, toInstall, res, inv.levels); // === Create new custom packs Dictionary <string, ulong> availableLevels = mainAssets.FindLevels(); foreach (LevelPack pack in inv.packs) { if (pack.name == null || pack.id == null || pack.levelIDs == null) { throw new ApplicationException("Packs require name, id and levelIDs list"); } var txn = new SerializedAssets.Transaction(mainAssets); CustomPackInfo info = LevelPackBehaviorData.CreateCustomPack( txn, pack.id, pack.name, pack.coverImagePath ); txn.ApplyTo(mainAssets); var customCollection = info.collection.FollowToScript <LevelCollectionBehaviorData>(mainAssets); foreach (string levelID in pack.levelIDs) { ulong levelPathID; if (!availableLevels.TryGetValue(levelID, out levelPathID)) { res.missingFromPacks.Add(levelID); continue; } customCollection.levels.Add(new AssetPtr(0, levelPathID)); } rootLevelPack.beatmapLevelPacks.Add(new AssetPtr(mainFileI, info.pack.pathID)); if (alwaysOwned != null) { alwaysOwned.levelPacks.Add(info.pack); } } res.presentLevels = availableLevels.Keys.ToList(); apk.ReplaceAssetsFile(apk.RootPackFile(), rootPackAssets.ToBytes()); }