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 void Main(string[] args) { if (args.Length < 1) { Console.WriteLine("arguments: pathToAPKFileToModify levelFolders..."); return; } string apkPath = args[0]; using (Apk apk = new Apk(apkPath)) { apk.PatchSignatureCheck(); byte[] data = apk.ReadEntireEntry(Apk.MainAssetsFile); SerializedAssets assets = SerializedAssets.FromBytes(data); HashSet <string> existingLevels = assets.ExistingLevelIDs(); LevelCollectionBehaviorData extrasCollection = assets.FindExtrasLevelCollection(); for (int i = 1; i < args.Length; i++) { Utils.FindLevels(args[i], levelFolder => { JsonLevel level = JsonLevel.LoadFromFolder(levelFolder); string levelID = level.LevelID(); if (existingLevels.Contains(levelID)) { Console.WriteLine($"Present: {level._songName}"); } else { Console.WriteLine($"Adding: {level._songName}"); AssetPtr levelPtr = level.AddToAssets(assets, apk); extrasCollection.levels.Add(levelPtr); existingLevels.Add(levelID); } }); } byte[] outData = assets.ToBytes(); apk.ReplaceAssetsFile(Apk.MainAssetsFile, outData); } }
static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine("arguments: pathToAPKFileToModify levelFolders..."); return; } string apkPath = args[0]; using (Apk apk = new Apk(apkPath)) { apk.PatchSignatureCheck(); byte[] data = apk.ReadEntireEntry(apk.MainAssetsFile()); SerializedAssets assets = SerializedAssets.FromBytes(data, apk.version); HashSet <string> existingLevels = assets.ExistingLevelIDs(); LevelCollectionBehaviorData extrasCollection = assets.FindExtrasLevelCollection(); for (int i = 1; i < args.Length; i++) { Utils.FindLevels(args[i], levelFolder => { try { JsonLevel level = JsonLevel.LoadFromFolder(levelFolder); string levelID = level.GenerateBasicLevelID(); if (existingLevels.Contains(levelID)) { Console.WriteLine($"Present: {level._songName}"); } else { Console.WriteLine($"Adding: {level._songName}"); // 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); existingLevels.Add(levelID); apkTxn.ApplyTo(apk); } } catch (FileNotFoundException e) { Console.WriteLine("[SKIPPING] Missing file referenced by level: {0}", e.FileName); } catch (JsonReaderException e) { Console.WriteLine("[SKIPPING] Invalid level JSON: {0}", e.Message); } }); } byte[] outData = assets.ToBytes(); apk.ReplaceAssetsFile(apk.MainAssetsFile(), outData); apk.Save(); } Console.WriteLine("Signing APK..."); Signer.Sign(apkPath); }
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()); }