Repository: Aeroluna/NoodleExtensions Branch: master Commit: 53a21b669fb4 Files: 76 Total size: 223.5 KB Directory structure: gitextract_67mkl1wr/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── Documentation/ │ ├── AnimationDocs.md │ └── examples/ │ └── documentationMap/ │ ├── README.md │ ├── cat.ogg │ ├── count.txt │ └── demo.js ├── LICENSE ├── NoodleExtensions/ │ ├── Animation/ │ │ ├── AnimationController.cs │ │ ├── AnimationHelper.cs │ │ ├── Events/ │ │ │ ├── AssignPlayerToTrack.cs │ │ │ ├── AssignTrackParent.cs │ │ │ └── NoodleEventData.cs │ │ ├── ParentObject.cs │ │ └── PlayerTrack.cs │ ├── CutoutManager.cs │ ├── Directory.Build.props │ ├── Directory.Build.targets │ ├── HarmonyPatches/ │ │ ├── BeatmapDataLoader.cs │ │ ├── BeatmapDataTransformHelper.cs │ │ ├── BeatmapObjectCallBackController.cs │ │ ├── BeatmapObjectManager.cs │ │ ├── BeatmapObjectSpawnController.cs │ │ ├── BeatmapObjectSpawnMovementData.cs │ │ ├── BeatmapObjectsInTimeRowProcessor.cs │ │ ├── Cutout/ │ │ │ ├── BaseNoteVisual.cs │ │ │ └── ObstacleDissolve.cs │ │ ├── FakeNotes/ │ │ │ ├── BadNoteCutEffectSpawner.cs │ │ │ ├── BeatmapData.cs │ │ │ ├── BeatmapObjectExecutionRatingsRecorder.cs │ │ │ ├── BeatmapObjectManager.cs │ │ │ ├── BombCutSoundEffectManager.cs │ │ │ ├── BombNoteController.cs │ │ │ ├── FakeNoteHelper.cs │ │ │ ├── GameEnergyCounter.cs │ │ │ ├── GameNoteController.cs │ │ │ ├── NoteCutCoreEffectSpawner.cs │ │ │ ├── NoteCutScoreSpawner.cs │ │ │ ├── NoteCutSoundEffectManager.cs │ │ │ ├── ObstacleSaberSparkleEffectManager.cs │ │ │ └── PlayerHeadAndObstacleInteraction.cs │ │ ├── GameplayCoreInstaller.cs │ │ ├── LeftHanded/ │ │ │ ├── BeatmapDataMirrorTransform.cs │ │ │ ├── NoteData.cs │ │ │ └── ObstacleData.cs │ │ ├── Mirror/ │ │ │ ├── MirroredNoteController.cs │ │ │ └── MirroredObstacleController.cs │ │ ├── MultiplayerConnectedPlayerInstaller.cs │ │ ├── NoteController.cs │ │ ├── NoteFloorMovement.cs │ │ ├── NoteJump.cs │ │ ├── ObstacleController.cs │ │ ├── SceneTransition/ │ │ │ ├── MissionLevelScenesTransitionSetupDataSO.cs │ │ │ ├── MultiplayerLevelScenesTransitionSetupDataSO.cs │ │ │ ├── SceneTransitionHelper.cs │ │ │ ├── StandardLevelScenesTransitionSetupDataSO.cs │ │ │ └── TutorialScenesTransitionSetupDataSO.cs │ │ ├── SmallFixes/ │ │ │ ├── BasicBeatmapObjectManager.cs │ │ │ ├── BeatEffectSpawner.cs │ │ │ ├── BeatmapObjectManager.cs │ │ │ ├── CutoutEffect.cs │ │ │ ├── DisappearingArrowController.cs │ │ │ ├── MemoryPoolBase.cs │ │ │ └── PlayerTransforms.cs │ │ ├── SpawnDataHelper.cs │ │ └── SpawnRotationProcessor.cs │ ├── NoodleController.cs │ ├── NoodleExtensions.csproj │ ├── NoodleExtensionsExtensions.cs │ ├── NoodleObjectData.cs │ ├── Plugin.cs │ └── manifest.json ├── NoodleExtensions.sln └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # NOTE: Requires **VS2019 16.3** or later # Code files [*.{cs,vb}] dotnet_diagnostic.CA1001.severity = warning dotnet_diagnostic.CA1009.severity = warning dotnet_diagnostic.CA1016.severity = warning dotnet_diagnostic.CA1033.severity = warning dotnet_diagnostic.CA1049.severity = warning dotnet_diagnostic.CA1060.severity = warning dotnet_diagnostic.CA1061.severity = warning dotnet_diagnostic.CA1063.severity = warning dotnet_diagnostic.CA1065.severity = warning dotnet_diagnostic.CA1301.severity = warning dotnet_diagnostic.CA1400.severity = warning dotnet_diagnostic.CA1401.severity = warning dotnet_diagnostic.CA1403.severity = warning dotnet_diagnostic.CA1404.severity = warning dotnet_diagnostic.CA1405.severity = warning dotnet_diagnostic.CA1410.severity = warning dotnet_diagnostic.CA1415.severity = warning dotnet_diagnostic.CA1821.severity = warning dotnet_diagnostic.CA1900.severity = warning dotnet_diagnostic.CA1901.severity = warning dotnet_diagnostic.CA2002.severity = warning dotnet_diagnostic.CA2100.severity = warning dotnet_diagnostic.CA2101.severity = warning dotnet_diagnostic.CA2108.severity = warning dotnet_diagnostic.CA2111.severity = warning dotnet_diagnostic.CA2112.severity = warning dotnet_diagnostic.CA2114.severity = warning dotnet_diagnostic.CA2116.severity = warning dotnet_diagnostic.CA2117.severity = warning dotnet_diagnostic.CA2122.severity = warning dotnet_diagnostic.CA2123.severity = warning dotnet_diagnostic.CA2124.severity = warning dotnet_diagnostic.CA2126.severity = warning dotnet_diagnostic.CA2131.severity = warning dotnet_diagnostic.CA2132.severity = warning dotnet_diagnostic.CA2133.severity = warning dotnet_diagnostic.CA2134.severity = warning dotnet_diagnostic.CA2137.severity = warning dotnet_diagnostic.CA2138.severity = warning dotnet_diagnostic.CA2140.severity = warning dotnet_diagnostic.CA2141.severity = warning dotnet_diagnostic.CA2146.severity = warning dotnet_diagnostic.CA2147.severity = warning dotnet_diagnostic.CA2149.severity = warning dotnet_diagnostic.CA2200.severity = warning dotnet_diagnostic.CA2202.severity = warning dotnet_diagnostic.CA2207.severity = warning dotnet_diagnostic.CA2212.severity = warning dotnet_diagnostic.CA2213.severity = warning dotnet_diagnostic.CA2214.severity = warning dotnet_diagnostic.CA2216.severity = warning dotnet_diagnostic.CA2220.severity = warning dotnet_diagnostic.CA2229.severity = warning dotnet_diagnostic.CA2231.severity = warning dotnet_diagnostic.CA2232.severity = warning dotnet_diagnostic.CA2235.severity = warning dotnet_diagnostic.CA2236.severity = warning dotnet_diagnostic.CA2237.severity = warning dotnet_diagnostic.CA2238.severity = warning dotnet_diagnostic.CA2240.severity = warning dotnet_diagnostic.CA2241.severity = warning dotnet_diagnostic.CA2242.severity = warning dotnet_diagnostic.CS8019.severity = warning dotnet_diagnostic.CS8020.severity = warning dotnet_diagnostic.IDE0001.severity = warning dotnet_diagnostic.IDE0002.severity = warning dotnet_diagnostic.IDE0060.severity = warning dotnet_diagnostic.SA0001.severity = none dotnet_diagnostic.SA1101.severity = none dotnet_diagnostic.SA1309.severity = none dotnet_diagnostic.SA1310.severity = none dotnet_diagnostic.SA1313.severity = none dotnet_diagnostic.SA1402.severity = none dotnet_diagnostic.SA1600.severity = none dotnet_diagnostic.SA1601.severity = none dotnet_diagnostic.SA1602.severity = none dotnet_diagnostic.SA1611.severity = none dotnet_diagnostic.SA1615.severity = none dotnet_diagnostic.SA1618.severity = none dotnet_diagnostic.SA1619.severity = none dotnet_diagnostic.SA1629.severity = none dotnet_diagnostic.SA1633.severity = none dotnet_diagnostic.SA1649.severity = none dotnet_diagnostic.SX1101.severity = warning dotnet_diagnostic.SX1309.severity = warning dotnet_diagnostic.SX1309S.severity = warning ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: aeroluna tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- Backup*.rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb ================================================ FILE: Documentation/AnimationDocs.md ================================================ These docs have moved! https://github.com/Aeroluna/Heck/wiki/Animation ================================================ FILE: Documentation/examples/documentationMap/README.md ================================================ # Documentation map This is a JS script used to generate the map seen in the documentation of noodle extensions. Most of it should be self explanatory, it requires nodeJS to run. If you have any simple questions feel free to dm me on discord `Reaxt#0690` ================================================ FILE: Documentation/examples/documentationMap/count.txt ================================================ 124 ================================================ FILE: Documentation/examples/documentationMap/demo.js ================================================ 'use strict' const fs = require('fs'); const INPUT = "ExpertPlusStandard.dat" const OUTPUT = "ExpertStandard.dat" let difficulty = JSON.parse(fs.readFileSync(INPUT)); //#region this just counts how many time you ran it for fun, feel free to remove. if(!(fs.existsSync("count.txt"))) { fs.writeFileSync("count.txt", parseInt("0").toString()) } let count = parseInt(fs.readFileSync("count.txt")) count++ fs.writeFileSync("count.txt", count.toString()) console.log("GIVE IT UP FOR RUN " + count) //#endregion difficulty._customData = { _pointDefinitions: [], _customEvents: [] }; const _customData = difficulty._customData; const _obstacles = difficulty._obstacles; const _notes = difficulty._notes; const _customEvents = _customData._customEvents; const _pointDefinitions = _customData._pointDefinitions; let filterednotes _obstacles.forEach(wall => { if (!wall._customData) { wall._customData = {} } }) _notes.forEach(note => { if (!note._customData) { note._customData = {} } }) //#region helper functions function round(value, decimals) { return Number(Math.round(value+'e'+decimals)+'e-'+decimals); } function getJumps(njs, offset) { const _startHalfJumpDurationInBeats = 4 const _maxHalfJumpDistance = 18 const _startBPM = 170 const bpm = 170 const _startNoteJumpMovementSpeed = njs const _noteJumpStartBeatOffset = offset let _noteJumpMovementSpeed = (_startNoteJumpMovementSpeed * bpm) / _startBPM let num = 60 / bpm let num2 = _startHalfJumpDurationInBeats while (_noteJumpMovementSpeed * num * num2 > _maxHalfJumpDistance) { num2 /= 2 } num2 += _noteJumpStartBeatOffset if (num2 < 1) { num2 = 1 } const _jumpDuration = num * num2 * 2 const _jumpDistance = _noteJumpMovementSpeed * _jumpDuration return {half: num2, dist: _jumpDistance} } function offestOnNotesBetween(p1,p2,offset) { filterednotes = _notes.filter(n => n._time >= p1 && n._time <= p2) filterednotes.forEach(object => { //always worth having. //man this shit BETTER not be undefined. if(typeof offset !== "undefined") {object._customData._noteJumpStartBeatOffset = offset;} }) return filterednotes; } function lerp(v0,v1,t) { return v0*(1-t)+v1*t; } function trackOnNotesBetween(track, p1,p2,potentialOffset) { filterednotes = _notes.filter(n => n._time >= p1 && n._time <= p2) filterednotes.forEach(object => { object._customData._track = track; if(typeof potentialOffset !== "undefined") {object._customData._noteJumpStartBeatOffset = potentialOffset;} }) return filterednotes; } //applies a track to notes on two tracks between two times based on the color of the notes //IT GONNA FUCK UP WITH BOMBS I TELL YOU HWAT BOI //red, blue, p1, p2, potentialOffset function trackOnNotesBetweenRBSep(trackR, trackB, p1, p2, potentialOffset){ filterednotes = _notes.filter(n => n._time >= p1 && n._time <= p2) filterednotes.forEach(object => { if(typeof potentialOffset !== "undefined") {object._customData._noteJumpStartBeatOffset = potentialOffset;} if(object._type == 0) {object._customData._track = trackR}; if(object._type == 1) {object._customData._track = trackB}; }) return filterednotes; } //p1, p2, potentialoffset, up, down, left, right, //TODO: ADD OTHER DIRS function trackOnNotesBetweenDirSep(p1, p2, potentialOffset, trackUp, trackDown, trackLeft, trackRight) { filterednotes = _notes.filter(n => n._time >= p1 && n._time <= p2) filterednotes.forEach(object => { if(object._cutDirection == 0 && typeof trackUp !== "undefined") {object._customData._track = trackUp} if(object._cutDirection == 1 && typeof trackUp !== "undefined") {object._customData._track = trackDown} if(object._cutDirection == 2 && typeof trackUp !== "undefined") {object._customData._track = trackLeft} if(object._cutDirection == 3 && typeof trackUp !== "undefined") {object._customData._track = trackRight} //i might want to make this only run if I assign a track... if(typeof potentialOffset !== "undefined") {object._customData._noteJumpStartBeatOffset = potentialOffset;} }) return filterednotes; } //#endregion //#region use this area to do your stuff //#region 4-10 _position demo trackOnNotesBetween("firstPositionDemo", 4, 20) //this function achieves the same as the lines below, its just a simpler/easier way to do so filterednotes = _notes.filter(n=> n._time >= 4 && n._time <= 25) filterednotes.forEach(note => { note._customData._track = "firstPositionDemo" }) //push demo point definitions _pointDefinitions.push({ "_name":"examplePositionPointDef", "_points":[ [0,0,0,0], [0,5,0,0.5,"splineCatmullRom"], [0,0,0,1,"splineCatmullRom"] ] }, { "_name":"examplePositionPath", "_points":[ [0,0,0,0], [0,5,0,0.25,"splineCatmullRom"], [0,0,0,0.5,"splineCatmullRom"] ] }) //animate track _customEvents.push({ "_time":4, "_type":"AnimateTrack", "_data":{ "_track":"firstPositionDemo", "_position":"examplePositionPointDef", "_duration":8 } }) //AssignPath _customEvents.push({ "_time":12, "_type":"AssignPathAnimation", "_data":{ "_track":"firstPositionDemo", "_position":"examplePositionPath", "_duration":4, "_easing":"easeInBounce" } }, { "_time":16, "_type":"AssignPathAnimation", "_data":{ "_track":"firstPositionDemo", "_position":[[0,0,0,0],[0,0,0,1]], "_duration":4, "_easing":"easeOutBounce" } }) //#endregion //#region _localRotation 20-40 trackOnNotesBetween("localRotationDemo", 20, 40) _pointDefinitions.push({ "_name":"localSpinDemoAnimate", "_points":[ [0,0,0,0], [90,0,0,0.25], [180,0,0,0.5], [270,0,0,0.75], [360,0,0,1] ] }, { "_name":"localSpinDemoAnimateRev", "_points":[ [0,0,0,0], [-90,0,0,0.25], [-180,0,0,0.5], [-270,0,0,0.75], [-360,0,0,1] ] }, { "_name":"localSpinDemoPath", "_points":[ [0,0,0,0], [0,0,90,0.125], [0,0,180,0.25], [0,0,270,0.375], [0,0,360,0.5] ] } ) //AnimateTrack, _customEvents.push({ "_time":20, "_type":"AnimateTrack", "_data":{ "_track":"localRotationDemo", "_duration":5, "_localRotation":"localSpinDemoAnimate", "_easing":"easeInOutExpo" } }, { "_time":25, "_type":"AnimateTrack", "_data":{ "_track":"localRotationDemo", "_duration":5, "_localRotation":"localSpinDemoAnimateRev", "_easing":"easeInOutExpo" } }, { "_time":30, "_type":"AssignPathAnimation", "_data":{ "_track":"localRotationDemo", "_duration":0, "_localRotation":"localSpinDemoPath" } }) //#endregion //#region _rotation 40-60 trackOnNotesBetween("RotationDemo", 40, 60) _pointDefinitions.push({ "_name":"RotationPointsAnimate", "_points":[ [0,0,0,0], [0,90,0,0.25], [0,180,0,0.5], [0,270,0,0.75], [0,360,0,1] ] }, { "_name":"RotationPointsPath", "_points":[ [0,0,0,0], [0,45,0,0.125, "splineCatmullRom"], [0,-45,0,0.25,"splineCatmullRom"], [0,22.5,0,0.375,"splineCatmullRom"], [0,-22.5,0,0.5,"splineCatmullRom"], [0,0,0,0.625,"splineCatmullRom"] ] } ) //AnimateTrack _customEvents.push({ "_time":40, "_type":"AnimateTrack", "_data":{ "_track":"RotationDemo", "_rotation":"RotationPointsAnimate", "_duration":10 } }, { "_time":50, "_type":"AssignPathAnimation", "_data":{ "_track":"RotationDemo", "_rotation":"RotationPointsPath", "_duration":5 } }, { "_time":55, "_type":"AssignPathAnimation", "_data":{ "_track":"RotationDemo", "_rotation":[[0,0,0,0]], "_duration":5 } }) //#endregion //#region _dissolve 60-80 trackOnNotesBetween("dissolveDemo", 60, 80) _pointDefinitions.push({ "_name":"dissolveDemoAnimate", "_points":[ [1,0], [0,0.25], [0.5,0.50], [0,0.75], [1,1] ] }, { "_name":"dissolveDemoPath", "_points":[ [0,0], [1,0.125], [1, 0.30], [0,0.35] ] } ) _customEvents.push({ "_time":60, "_type":"AnimateTrack", "_data":{ "_track":"dissolveDemo", "_dissolve":"dissolveDemoAnimate", "_duration":10 } }, { "_time":70, "_type":"AssignPathAnimation", "_data":{ "_track":"dissolveDemo", "_dissolve":"dissolveDemoPath", "_duration":0 } }) //#endregion //#region _dissolve 80-100 trackOnNotesBetween("dissolveArrowDemo", 80, 100) _pointDefinitions.push({ "_name":"dissolveArrowDemoAnimate", "_points":[ [1,0], [0,1] ] }, { "_name":"dissolveArrowDemoPath", "_points":[ [0,0.10], [1,0.20], [1, 0.30], [0,0.35] ] } ) _customEvents.push({ "_time":80, "_type":"AnimateTrack", "_data":{ "_track":"dissolveArrowDemo", "_dissolveArrow":"dissolveArrowDemoAnimate", "_duration":5 } }, { "_time":85, "_type":"AnimateTrack", "_data":{ "_track":"dissolveArrowDemo", "_dissolveArrow":[[0,0],[1,1]], "_duration":5 } }, { "_time":90, "_type":"AssignPathAnimation", "_data":{ "_track":"dissolveArrowDemo", "_dissolveArrow":"dissolveArrowDemoPath" } }) //#endregion //#region color walls 100-120 _obstacles.push({ "_time":100, "_duration":9, "_lineindex":0, "_type":0, "_width":1, "_customData":{ "_track":"LeftColorWall" } }, { "_time":100, "_duration":9, "_lineindex":3, "_type":0, "_width":1, "_customData":{ "_track":"RightColorWall" } }) _pointDefinitions.push({ "_name":"RightColorWallAnimate", "_points":[ [1,0,0,1,0.2], [0,1,0,1,0.4], [0,0,1,1,0.6], [0,1,1,1,0.8], [1,1,1,1,1], ] }, { "_name":"LeftColorWallAnimate", "_points":[ [1,0,0,0,0.2], [0,1,0,0,0.4], [0,0,1,0,0.6], [0,1,1,0,0.8], [1,1,1,0,1], ] } ) _customEvents.push({ "_time":98, "_type":"AnimateTrack", "_data":{ "_track":"RightColorWall", "_color":"RightColorWallAnimate", "_duration":10 } }, { "_time":98, "_type":"AnimateTrack", "_data":{ "_track":"LeftColorWall", "_color":"LeftColorWallAnimate", "_duration":10 } } ) for (let i = 0; i < 400; i++) { let startTime = 110; let interval = 0.025; let duration = 0.01; _obstacles.push({ "_time":startTime+(interval*i), "_duration":duration, "_lineindex":0, "_type":1, "_width":1, "_customData":{ "_track":"LeftColorWallStatic" } }, { "_time":startTime+(interval*i), "_duration":duration, "_lineindex":3, "_type":1, "_width":1, "_customData":{ "_track":"RightColorWallStatic" } }) } _pointDefinitions.push({ "_name":"GradientPathOne", "_points":[ [1,0,0,0.5,0.0416], [0,1,0,0.5,0.0832], [0,0,1,0.5,0.1248], [1,0,0,0.5,0.1664], [0,1,0,0.5,0.208], [0,0,1,0.5,0.2496], [1,0,0,0.5,0.2912], [0,1,0,0.5,0.3328], [0,0,1,0.5,0.3743], [1,0,0,0.5,0.416], [0,1,0,0.5,0.4576], [0,0,1,0.5,0.4992] ] }, { "_name":"GradientPathTwo", "_points":[ [0,1,0,0.5,0.0416], [0,0,1,0.5,0.0832], [1,0,0,0.5,0.1248], [0,1,0,0.5,0.1664], [0,0,1,0.5,0.208], [1,0,0,0.5,0.2496], [0,1,0,0.5,0.2912], [0,0,1,0.5,0.3328], [1,0,0,0.5,0.3743], [0,1,0,0.5,0.416], [0,0,1,0.5,0.4576], [1,0,0,0.5,0.4992] ] } ) _customEvents.push( { "_time":110, "_type":"AssignPathAnimation", "_data":{ "_track":"RightColorWallStatic", "_color":"GradientPathOne", "_duration":2 } }, { "_time":114, "_type":"AssignPathAnimation", "_data":{ "_track":"RightColorWallStatic", "_color":"GradientPathTwo", "_duration":6, "_easing":"easeOutElastic", } }, { "_time":110, "_type":"AssignPathAnimation", "_data":{ "_track":"LeftColorWallStatic", "_color":"GradientPathTwo", "_duration":2 } }, { "_time":114, "_type":"AssignPathAnimation", "_data":{ "_track":"LeftColorWallStatic", "_color":"GradientPathOne", "_duration":6, "_easing":"easeOutElastic" } }) //#endregion //#region definitePosition 130-150 trackOnNotesBetween("definitePosDemo", 130, 150) _pointDefinitions.push({ "_name":"defPosPath", "_points":[ [0, 0, 20, 0], [10, 0, 20, 0.1], [10, 10, 20, 0.2], [0, 10, 20, 0.3], [0, 0, 20, 0.4], [0, 0, 10, 0.5], [-20, 0, 10, 1.0] ] }, { "_name":"defPosNormal", "_points":[ [0,0,23,0], [0,0,0,0.5], [0,0,-23,1] ] } ) _customEvents.push({ "_time":132, "_type":"AssignPathAnimation", "_data":{ "_track":"definitePosDemo", "_definitePosition":"defPosPath", "_duration":3 } }, { "_time":0, "_type":"AssignPathAnimation", "_data":{ "_track":"definitePosDemo", "_definitePosition":"defPosNormal", "_duration":0 } }) //#endregion //#region time 155 (this example is bad, you basically need to script time. Better example coming someday) _pointDefinitions.push({ "_name":"SingleNoteTime", "_points":[ [0,0], [0.45, 0.15], [0.15, 0.30], [0.5, 0.5], [1,1] ] }) trackOnNotesBetween("singleNoteTimeTrack", 155, 155) _customEvents.push({ "_time":153, "_type":"AnimateTrack", "_data":{ "_time":"SingleNoteTime", "_duration":10, "_track":"singleNoteTimeTrack" } }) //#endregion //#region scale 165-190 trackOnNotesBetween("scaleTrack", 165, 195) for (let i = 0; i < 30; i++) { let mult = 1 let dur = 0.5 let scaleVar = 3 _obstacles.push({ "_time":165+(i*mult), "_duration":dur, "_lineIndex":0, "_type":0, "_width":0, "_customData":{ "_position":[-6,1], "_scale":[1+(Math.random()*scaleVar),1+(Math.random()*scaleVar)], "_track":"scaleTrack" } }) } _pointDefinitions.push({ "_name":"AnimateTrackScale", "_points":[ [1,1,1,0], [0.80,0.80,0.80,0.15,"easeOutCirc"], [2,2,2,0.5,"easeOutBounce"], [2,2,2,0.6], [2.5,1,1,0.8,"easeOutExpo"], [1,1,1,1,"easeOutBounce"] ] }, { "_name":"PathScale", "_points":[ [1,1,1,0], [4,0.5,1,0.20,"easeInElastic"], [1,1,1,0.50,"easeOutElastic"] ] }) _customEvents.push({ "_time":165, "_type":"AnimateTrack", "_data":{ "_track":"scaleTrack", "_scale":"AnimateTrackScale", "_duration":5 } }, { "_time":175, "_type":"AssignPathAnimation", "_data":{ "_track":"scaleTrack", "_scale":"PathScale" } }) //#endregion //#region write file const precision = 4 //decimals to round to const jsonP = Math.pow(10, precision) const sortP = Math.pow(10, 2) function deeperDaddy(obj) { if (obj) for (const key in obj) { if (obj[key] == null) { delete obj[key] } else if (typeof obj[key] === 'object' || Array.isArray(obj[key])) { deeperDaddy(obj[key]) } else if (typeof obj[key] == 'number') { obj[key] = parseFloat(Math.round((obj[key] + Number.EPSILON) * jsonP) / jsonP) } } } deeperDaddy(difficulty) difficulty._notes.sort( (a, b) => parseFloat(Math.round((a._time + Number.EPSILON) * sortP) / sortP) - parseFloat(Math.round((b._time + Number.EPSILON) * sortP) / sortP) || parseFloat(Math.round((a._lineIndex + Number.EPSILON) * sortP) / sortP) - parseFloat(Math.round((b._lineIndex + Number.EPSILON) * sortP) / sortP) || parseFloat(Math.round((a._lineLayer + Number.EPSILON) * sortP) / sortP) - parseFloat(Math.round((b._lineLayer + Number.EPSILON) * sortP) / sortP) ) difficulty._obstacles.sort((a, b) => a._time - b._time) difficulty._events.sort((a, b) => a._time - b._time) fs.writeFileSync(OUTPUT, JSON.stringify(difficulty, null, 0)); //#endregion ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Aeroluna Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NoodleExtensions/Animation/AnimationController.cs ================================================ namespace NoodleExtensions.Animation { using CustomJSONData; using UnityEngine; public class AnimationController : MonoBehaviour { public static AnimationController? Instance { get; private set; } public CustomEventCallbackController? CustomEventCallbackController { get; private set; } internal static void CustomEventCallbackInit(CustomEventCallbackController customEventCallbackController) { if (customEventCallbackController.BeatmapData?.customData.Get("isMultiplayer") ?? false) { return; } if (Instance != null) { Destroy(Instance); } Instance = customEventCallbackController.gameObject.AddComponent(); Instance.CustomEventCallbackController = customEventCallbackController; Instance.CustomEventCallbackController.AddCustomEventCallback(AssignPlayerToTrack.Callback); Instance.CustomEventCallbackController.AddCustomEventCallback(AssignTrackParent.Callback); } } } ================================================ FILE: NoodleExtensions/Animation/AnimationHelper.cs ================================================ namespace NoodleExtensions.Animation { using System.Collections.Generic; using System.Linq; using Heck.Animation; using UnityEngine; using static Heck.Animation.AnimationHelper; using static Heck.NullableExtensions; using static NoodleExtensions.HarmonyPatches.SpawnDataHelper.BeatmapObjectSpawnMovementDataVariables; using static NoodleExtensions.Plugin; public static class AnimationHelper { private static BasicBeatmapObjectManager? BeatmapObjectManager => HarmonyPatches.BeatmapObjectSpawnControllerStart.BeatmapObjectManager; internal static void OnTrackCreated(Track track) { track.AddProperty(POSITION, PropertyType.Vector3); track.AddProperty(ROTATION, PropertyType.Quaternion); track.AddProperty(SCALE, PropertyType.Vector3); track.AddProperty(LOCALROTATION, PropertyType.Quaternion); track.AddProperty(DISSOLVE, PropertyType.Linear); track.AddProperty(DISSOLVEARROW, PropertyType.Linear); track.AddProperty(TIME, PropertyType.Linear); track.AddProperty(CUTTABLE, PropertyType.Linear); track.AddPathProperty(POSITION, PropertyType.Vector3); track.AddPathProperty(ROTATION, PropertyType.Quaternion); track.AddPathProperty(SCALE, PropertyType.Vector3); track.AddPathProperty(LOCALROTATION, PropertyType.Quaternion); track.AddPathProperty(DEFINITEPOSITION, PropertyType.Vector3); track.AddPathProperty(DISSOLVE, PropertyType.Linear); track.AddPathProperty(DISSOLVEARROW, PropertyType.Linear); track.AddPathProperty(CUTTABLE, PropertyType.Linear); } internal static void GetDefinitePositionOffset(NoodleObjectData.AnimationObjectData? animationObject, IEnumerable? tracks, float time, out Vector3? definitePosition) { Vector3? pathDefinitePosition = animationObject?.LocalDefinitePosition?.Interpolate(time); if (!pathDefinitePosition.HasValue && tracks != null) { if (tracks.Count() > 1) { pathDefinitePosition = SumVectorNullables(tracks.Select(n => TryGetVector3PathProperty(n, DEFINITEPOSITION, time))); } else { pathDefinitePosition = TryGetVector3PathProperty(tracks.First(), DEFINITEPOSITION, time); } } if (pathDefinitePosition.HasValue) { Vector3? pathPosition = animationObject?.LocalPosition?.Interpolate(time); Vector3? positionOffset = null; if (tracks != null) { if (tracks.Count() > 1) { pathPosition ??= SumVectorNullables(tracks.Select(n => TryGetVector3PathProperty(n, POSITION, time))); positionOffset = SumVectorNullables(SumVectorNullables(tracks.Select(n => TryGetProperty(n, POSITION))), pathPosition); } else { Track track = tracks.First(); pathPosition ??= TryGetVector3PathProperty(track, POSITION, time); positionOffset = SumVectorNullables(TryGetProperty(track, POSITION), pathPosition); } } else { positionOffset = pathPosition; } definitePosition = SumVectorNullables(positionOffset, pathDefinitePosition) * NoteLinesDistance; if (LeftHandedMode) { MirrorVectorNullable(ref definitePosition); } } else { definitePosition = null; } } internal static void GetObjectOffset( NoodleObjectData.AnimationObjectData? animationObject, IEnumerable? tracks, float time, out Vector3? positionOffset, out Quaternion? rotationOffset, out Vector3? scaleOffset, out Quaternion? localRotationOffset, out float? dissolve, out float? dissolveArrow, out float? cuttable) { /* * position = SumVectorNullables * rotation = MultQuaternionNullables * scale = MultVectorNullables * localRotation = MultQuaternionNullables * dissolve = MultFloatNullables * dissolveArrow = MultFloatNullables * cuttable = MultFloatNullables */ Vector3? pathPosition = null; Quaternion? pathRotation = null; Vector3? pathScale = null; Quaternion? pathLocalRotation = null; float? pathDissolve = null; float? pathDissolveArrow = null; float? pathCuttable = null; if (animationObject != null) { pathPosition = animationObject.LocalPosition?.Interpolate(time); pathRotation = animationObject.LocalRotation?.InterpolateQuaternion(time); pathScale = animationObject.LocalScale?.Interpolate(time); pathLocalRotation = animationObject.LocalLocalRotation?.InterpolateQuaternion(time); pathDissolve = animationObject.LocalDissolve?.InterpolateLinear(time); pathDissolveArrow = animationObject.LocalDissolveArrow?.InterpolateLinear(time); pathCuttable = animationObject.LocalCuttable?.InterpolateLinear(time); } if (tracks != null) { Vector3? trackPosition; Quaternion? trackRotation; Vector3? trackScale; Quaternion? trackLocalRotation; float? trackDissolve; float? trackDissolveArrow; float? trackCuttable; if (tracks.Count() > 1) { pathPosition ??= SumVectorNullables(tracks.Select(n => TryGetVector3PathProperty(n, POSITION, time))); pathRotation ??= MultQuaternionNullables(tracks.Select(n => TryGetQuaternionPathProperty(n, ROTATION, time))); pathScale ??= MultVectorNullables(tracks.Select(n => TryGetVector3PathProperty(n, SCALE, time))); pathLocalRotation ??= MultQuaternionNullables(tracks.Select(n => TryGetQuaternionPathProperty(n, LOCALROTATION, time))); pathDissolve ??= MultFloatNullables(tracks.Select(n => TryGetLinearPathProperty(n, DISSOLVE, time))); pathDissolveArrow ??= MultFloatNullables(tracks.Select(n => TryGetLinearPathProperty(n, DISSOLVEARROW, time))); pathCuttable ??= MultFloatNullables(tracks.Select(n => TryGetLinearPathProperty(n, CUTTABLE, time))); trackPosition = SumVectorNullables(tracks.Select(n => TryGetProperty(n, POSITION))); trackRotation = MultQuaternionNullables(tracks.Select(n => TryGetProperty(n, ROTATION))); trackScale = MultVectorNullables(tracks.Select(n => TryGetProperty(n, SCALE))); trackLocalRotation = MultQuaternionNullables(tracks.Select(n => TryGetProperty(n, LOCALROTATION))); trackDissolve = MultFloatNullables(tracks.Select(n => TryGetProperty(n, DISSOLVE))); trackDissolveArrow = MultFloatNullables(tracks.Select(n => TryGetProperty(n, DISSOLVEARROW))); trackCuttable = MultFloatNullables(tracks.Select(n => TryGetProperty(n, CUTTABLE))); } else { Track track = tracks.First(); pathPosition ??= TryGetVector3PathProperty(track, POSITION, time); pathRotation ??= TryGetQuaternionPathProperty(track, ROTATION, time); pathScale ??= TryGetVector3PathProperty(track, SCALE, time); pathLocalRotation ??= TryGetQuaternionPathProperty(track, LOCALROTATION, time); pathDissolve ??= TryGetLinearPathProperty(track, DISSOLVE, time); pathDissolveArrow ??= TryGetLinearPathProperty(track, DISSOLVEARROW, time); pathCuttable ??= TryGetLinearPathProperty(track, CUTTABLE, time); trackPosition = TryGetProperty(track, POSITION); trackRotation = TryGetProperty(track, ROTATION); trackScale = TryGetProperty(track, SCALE); trackLocalRotation = TryGetProperty(track, LOCALROTATION); trackDissolve = TryGetProperty(track, DISSOLVE); trackDissolveArrow = TryGetProperty(track, DISSOLVEARROW); trackCuttable = TryGetProperty(track, CUTTABLE); } positionOffset = SumVectorNullables(trackPosition, pathPosition) * NoteLinesDistance; rotationOffset = MultQuaternionNullables(trackRotation, pathRotation); scaleOffset = MultVectorNullables(trackScale, pathScale); localRotationOffset = MultQuaternionNullables(trackLocalRotation, pathLocalRotation); dissolve = MultFloatNullables(trackDissolve, pathDissolve); dissolveArrow = MultFloatNullables(trackDissolveArrow, pathDissolveArrow); cuttable = MultFloatNullables(trackCuttable, pathCuttable); } else { positionOffset = pathPosition * NoteLinesDistance; rotationOffset = pathRotation; scaleOffset = pathScale; localRotationOffset = pathLocalRotation; dissolve = pathDissolve; dissolveArrow = pathDissolveArrow; cuttable = pathCuttable; } if (LeftHandedMode) { MirrorVectorNullable(ref positionOffset); MirrorQuaternionNullable(ref rotationOffset); MirrorQuaternionNullable(ref localRotationOffset); } } internal static void GetAllPointData( Dictionary customData, Dictionary pointDefinitions, out PointDefinition? position, out PointDefinition? rotation, out PointDefinition? scale, out PointDefinition? localRotation, out PointDefinition? dissolve, out PointDefinition? dissolveArrow, out PointDefinition? cuttable, out PointDefinition? definitePosition) { TryGetPointData(customData, POSITION, out position, pointDefinitions); TryGetPointData(customData, ROTATION, out rotation, pointDefinitions); TryGetPointData(customData, SCALE, out scale, pointDefinitions); TryGetPointData(customData, LOCALROTATION, out localRotation, pointDefinitions); TryGetPointData(customData, DISSOLVE, out dissolve, pointDefinitions); TryGetPointData(customData, DISSOLVEARROW, out dissolveArrow, pointDefinitions); TryGetPointData(customData, CUTTABLE, out cuttable, pointDefinitions); TryGetPointData(customData, DEFINITEPOSITION, out definitePosition, pointDefinitions); } } } ================================================ FILE: NoodleExtensions/Animation/Events/AssignPlayerToTrack.cs ================================================ namespace NoodleExtensions.Animation { using System.Collections.Generic; using CustomJSONData; using CustomJSONData.CustomBeatmap; using Heck.Animation; using static NoodleExtensions.Animation.NoodleEventDataManager; using static NoodleExtensions.Plugin; internal static class AssignPlayerToTrack { internal static void OnTrackManagerCreated(TrackBuilder trackManager, CustomBeatmapData customBeatmapData) { List customEventsData = customBeatmapData.customEventsData; foreach (CustomEventData customEventData in customEventsData) { if (customEventData.type == ASSIGNPLAYERTOTRACK) { string? trackName = customEventData.data.Get(TRACK); if (trackName != null) { trackManager.AddTrack(trackName); } } } } internal static void Callback(CustomEventData customEventData) { if (customEventData.type == ASSIGNPLAYERTOTRACK) { NoodlePlayerTrackEventData? noodleData = TryGetEventData(customEventData); if (noodleData != null) { Track track = noodleData.Track; if (track != null) { PlayerTrack.AssignTrack(track); } } } } } } ================================================ FILE: NoodleExtensions/Animation/Events/AssignTrackParent.cs ================================================ namespace NoodleExtensions.Animation { using System.Collections.Generic; using CustomJSONData; using CustomJSONData.CustomBeatmap; using Heck.Animation; using static NoodleExtensions.Animation.NoodleEventDataManager; using static NoodleExtensions.Plugin; internal class AssignTrackParent { internal static void OnTrackManagerCreated(TrackBuilder trackManager, CustomBeatmapData customBeatmapData) { List customEventsData = customBeatmapData.customEventsData; foreach (CustomEventData customEventData in customEventsData) { if (customEventData.type == ASSIGNTRACKPARENT) { string? trackName = customEventData.data.Get("_parentTrack"); if (trackName != null) { trackManager.AddTrack(trackName); } } } } internal static void Callback(CustomEventData customEventData) { if (customEventData.type == ASSIGNTRACKPARENT) { NoodleParentTrackEventData? noodleData = TryGetEventData(customEventData); if (noodleData != null) { IEnumerable tracks = noodleData.ChildrenTracks; Track parentTrack = noodleData.ParentTrack; if (tracks != null && parentTrack != null) { ParentObject.AssignTrack(tracks, parentTrack, noodleData.Position, noodleData.Rotation, noodleData.LocalRotation, noodleData.Scale); } else { Logger.Log($"Missing _parentTrack or _childrenTracks!", IPA.Logging.Logger.Level.Error); } } } } } } ================================================ FILE: NoodleExtensions/Animation/Events/NoodleEventData.cs ================================================ namespace NoodleExtensions.Animation { using System; using System.Collections.Generic; using System.Linq; using CustomJSONData; using CustomJSONData.CustomBeatmap; using Heck.Animation; using UnityEngine; using static Heck.Animation.AnimationHelper; using static NoodleExtensions.Plugin; internal static class NoodleEventDataManager { private static Dictionary _noodleEventDatas = new Dictionary(); internal static T? TryGetEventData(CustomEventData customEventData) { if (_noodleEventDatas.TryGetValue(customEventData, out NoodleEventData noodleEventData)) { if (noodleEventData is T t) { return t; } else { throw new InvalidOperationException($"NoodleEventData was not of correct type. Expected: {typeof(T).Name}, was: {noodleEventData.GetType().Name}"); } } return default; } internal static void DeserializeBeatmapData(IReadonlyBeatmapData beatmapData) { _noodleEventDatas = new Dictionary(); foreach (CustomEventData customEventData in ((CustomBeatmapData)beatmapData).customEventsData) { try { NoodleEventData noodleEventData; switch (customEventData.type) { case ASSIGNPLAYERTOTRACK: noodleEventData = new NoodlePlayerTrackEventData(GetTrack(customEventData.data, beatmapData) ?? throw new InvalidOperationException("Track was not defined.")); break; case ASSIGNTRACKPARENT: noodleEventData = ProcessParentTrackEvent(customEventData.data, beatmapData); break; default: continue; } if (noodleEventData != null) { _noodleEventDatas.Add(customEventData, noodleEventData); } } catch (Exception e) { Plugin.Logger.Log($"Could not create NoodleEventData for event {customEventData.type} at {customEventData.time}", IPA.Logging.Logger.Level.Error); Plugin.Logger.Log(e, IPA.Logging.Logger.Level.Error); } } } private static NoodleParentTrackEventData ProcessParentTrackEvent(Dictionary customData, IReadonlyBeatmapData beatmapData) { IEnumerable? position = customData.Get>(POSITION)?.Select(n => Convert.ToSingle(n)); Vector3? posVector = null; if (position != null) { posVector = new Vector3(position.ElementAt(0), position.ElementAt(1), position.ElementAt(2)); } IEnumerable? rotation = customData.Get>(ROTATION)?.Select(n => Convert.ToSingle(n)); Quaternion? rotQuaternion = null; if (rotation != null) { rotQuaternion = Quaternion.Euler(rotation.ElementAt(0), rotation.ElementAt(1), rotation.ElementAt(2)); } IEnumerable? localrot = customData.Get>(LOCALROTATION)?.Select(n => Convert.ToSingle(n)); Quaternion? localRotQuaternion = null; if (localrot != null) { localRotQuaternion = Quaternion.Euler(localrot.ElementAt(0), localrot.ElementAt(1), localrot.ElementAt(2)); } IEnumerable? scale = customData.Get>(SCALE)?.Select(n => Convert.ToSingle(n)); Vector3? scaleVector = null; if (scale != null) { scaleVector = new Vector3(scale.ElementAt(0), scale.ElementAt(1), scale.ElementAt(2)); } return new NoodleParentTrackEventData( GetTrack(customData, beatmapData, "_parentTrack") ?? throw new InvalidOperationException("Parent track was not defined."), GetTrackArray(customData, beatmapData, "_childrenTracks") ?? throw new InvalidOperationException("Children track was not defined."), posVector, rotQuaternion, localRotQuaternion, scaleVector); } } internal record NoodlePlayerTrackEventData : NoodleEventData { internal NoodlePlayerTrackEventData(Track track) { Track = track; } internal Track Track { get; set; } } internal record NoodleParentTrackEventData : NoodleEventData { internal NoodleParentTrackEventData(Track parentTrack, IEnumerable childrenTracks, Vector3? position, Quaternion? rotation, Quaternion? localRotation, Vector3? scale) { ParentTrack = parentTrack; ChildrenTracks = childrenTracks; Position = position; Rotation = rotation; LocalRotation = localRotation; Scale = scale; } internal Track ParentTrack { get; } internal IEnumerable ChildrenTracks { get; } internal Vector3? Position { get; } internal Quaternion? Rotation { get; } internal Quaternion? LocalRotation { get; } internal Vector3? Scale { get; } } internal record NoodleEventData { } } ================================================ FILE: NoodleExtensions/Animation/ParentObject.cs ================================================ namespace NoodleExtensions.Animation { using System.Collections.Generic; using System.Linq; using Heck.Animation; using UnityEngine; using static Heck.Animation.AnimationHelper; using static Heck.NullableExtensions; using static NoodleExtensions.HarmonyPatches.SpawnDataHelper.BeatmapObjectSpawnMovementDataVariables; using static NoodleExtensions.Plugin; internal class ParentObject : MonoBehaviour { private Track? _track; private Transform? _origin; private Vector3 _startPos = Vector3.zero; private Quaternion _startRot = Quaternion.identity; private Quaternion _startLocalRot = Quaternion.identity; private Vector3 _startScale = Vector3.one; internal static ParentController? Controller { get; private set; } internal HashSet ChildrenTracks { get; } = new HashSet(); internal static void AssignTrack(IEnumerable tracks, Track parentTrack, Vector3? startPos, Quaternion? startRot, Quaternion? startLocalRot, Vector3? startScale) { if (tracks.Contains(parentTrack)) { throw new System.InvalidOperationException("How could a track contain itself?"); } if (Controller == null) { GameObject gameObject = new GameObject("ParentController"); Controller = gameObject.AddComponent(); } GameObject parentGameObject = new GameObject("ParentObject"); ParentObject instance = parentGameObject.AddComponent(); instance._origin = parentGameObject.transform; instance._track = parentTrack; Transform transform = instance.transform; if (startPos.HasValue) { instance._startPos = startPos.Value; transform.localPosition = instance._startPos * NoteLinesDistance; } if (startRot.HasValue) { instance._startRot = startRot.Value; instance._startLocalRot = instance._startRot; transform.localPosition = instance._startRot * transform.localPosition; transform.localRotation = instance._startRot; } if (startLocalRot.HasValue) { instance._startLocalRot = instance._startRot * startLocalRot.Value; transform.localRotation *= instance._startLocalRot; } if (startScale.HasValue) { instance._startScale = startScale.Value; transform.localScale = instance._startScale; } parentTrack.AddGameObject(parentGameObject); foreach (Track track in tracks) { foreach (ParentObject parentObject in Controller.ParentObjects) { track.OnGameObjectAdded -= parentObject.OnTrackGameObjectAdded; track.OnGameObjectRemoved -= parentObject.OnTrackGameObjectRemoved; parentObject.ChildrenTracks.Remove(track); } foreach (GameObject gameObject in track.GameObjects) { instance.ParentToObject(gameObject.transform); } instance.ChildrenTracks.Add(track); track.OnGameObjectAdded += instance.OnTrackGameObjectAdded; track.OnGameObjectRemoved += instance.OnTrackGameObjectRemoved; } Controller.ParentObjects.Add(instance); } private static void ResetTransformParent(Transform transform) { transform.SetParent(null, false); } private void OnTrackGameObjectAdded(GameObject gameObject) { ParentToObject(gameObject.transform); } private void OnTrackGameObjectRemoved(GameObject gameObject) { ResetTransformParent(gameObject.transform); } private void ParentToObject(Transform transform) { transform.SetParent(_origin!.transform, false); } private void OnDestroy() { foreach (Track track in ChildrenTracks) { track.OnGameObjectAdded -= OnTrackGameObjectAdded; track.OnGameObjectRemoved -= OnTrackGameObjectRemoved; } } private void Update() { Quaternion? rotation = TryGetProperty(_track, ROTATION); if (rotation.HasValue) { if (LeftHandedMode) { MirrorQuaternionNullable(ref rotation); } } Vector3? position = TryGetProperty(_track, POSITION); if (position.HasValue) { if (LeftHandedMode) { MirrorVectorNullable(ref position); } } Quaternion worldRotationQuatnerion = _startRot; Vector3 positionVector = worldRotationQuatnerion * (_startPos * NoteLinesDistance); if (rotation.HasValue || position.HasValue) { Quaternion rotationOffset = rotation ?? Quaternion.identity; worldRotationQuatnerion *= rotationOffset; Vector3 positionOffset = position ?? Vector3.zero; positionVector = worldRotationQuatnerion * ((positionOffset + _startPos) * NoteLinesDistance); } worldRotationQuatnerion *= _startLocalRot; Quaternion? localRotation = TryGetProperty(_track, LOCALROTATION); if (localRotation.HasValue) { if (LeftHandedMode) { MirrorQuaternionNullable(ref localRotation); } worldRotationQuatnerion *= localRotation!.Value; } Vector3 scaleVector = _startScale; Vector3? scale = TryGetProperty(_track, SCALE); if (scale.HasValue) { scaleVector = Vector3.Scale(_startScale, scale.Value); } if (_origin!.localRotation != worldRotationQuatnerion) { _origin.localRotation = worldRotationQuatnerion; } if (_origin.localPosition != positionVector) { _origin.localPosition = positionVector; } if (_origin.localScale != scaleVector) { _origin.localScale = scaleVector; } } } internal class ParentController : MonoBehaviour { internal HashSet ParentObjects { get; } = new HashSet(); internal ParentObject? GetParentObjectTrack(Track track) { ParentObject filteredParent = ParentObjects.FirstOrDefault(n => n.ChildrenTracks.Contains(track)); if (filteredParent != null) { return filteredParent; } else { return null; } } internal ParentObject? GetParentObjectTrackArray(IEnumerable tracks) { ParentObject filteredParent = ParentObjects.FirstOrDefault(n => n.ChildrenTracks.Intersect(tracks).Any()); if (filteredParent != null) { return filteredParent; } else { return null; } } private void OnDestroy() { ParentObjects.Clear(); } } } ================================================ FILE: NoodleExtensions/Animation/PlayerTrack.cs ================================================ namespace NoodleExtensions.Animation { using Heck.Animation; using IPA.Utilities; using UnityEngine; using static Heck.Animation.AnimationHelper; using static Heck.NullableExtensions; using static NoodleExtensions.HarmonyPatches.SpawnDataHelper.BeatmapObjectSpawnMovementDataVariables; using static NoodleExtensions.Plugin; internal class PlayerTrack : MonoBehaviour { private static readonly FieldAccessor.Accessor _pausedAccessor = FieldAccessor.GetAccessor("_paused"); private static PlayerTrack? _instance; private static Track? _track; private static Vector3 _startPos = Vector3.zero; private static Quaternion _startRot = Quaternion.identity; private static Quaternion _startLocalRot = Quaternion.identity; private static Transform? _origin; private static PauseController? _pauseController; internal static void AssignTrack(Track track) { if (_instance == null) { GameObject gameObject = GameObject.Find("LocalPlayerGameCore"); GameObject noodleObject = new GameObject("NoodlePlayerTrack"); _origin = noodleObject.transform; _origin.SetParent(gameObject.transform.parent, true); gameObject.transform.SetParent(_origin, true); _instance = noodleObject.AddComponent(); _pauseController = FindObjectOfType(); if (_pauseController != null) { _pauseController.didPauseEvent += _instance.OnDidPauseEvent; } _startLocalRot = _origin.localRotation; _startPos = _origin.localPosition; } _track = track; } private void OnDidPauseEvent() { _origin!.localRotation = _startLocalRot; _origin!.localPosition = _startPos; } private void OnDestroy() { if (_pauseController != null) { _pauseController.didPauseEvent -= OnDidPauseEvent; } } private void Update() { bool paused = false; if (_pauseController != null) { paused = _pausedAccessor(ref _pauseController); } if (!paused) { Quaternion? rotation = TryGetProperty(_track, ROTATION); if (rotation.HasValue) { if (LeftHandedMode) { MirrorQuaternionNullable(ref rotation); } } Vector3? position = TryGetProperty(_track, POSITION); if (position.HasValue) { if (LeftHandedMode) { MirrorVectorNullable(ref position); } } Quaternion worldRotationQuatnerion = _startRot; Vector3 positionVector = _startPos; if (rotation.HasValue || position.HasValue) { Quaternion finalRot = rotation ?? Quaternion.identity; worldRotationQuatnerion *= finalRot; Vector3 finalPos = position ?? Vector3.zero; positionVector = worldRotationQuatnerion * ((finalPos * NoteLinesDistance) + _startPos); } worldRotationQuatnerion *= _startLocalRot; Quaternion? localRotation = TryGetProperty(_track, LOCALROTATION); if (localRotation.HasValue) { if (LeftHandedMode) { MirrorQuaternionNullable(ref localRotation); } worldRotationQuatnerion *= localRotation!.Value; } if (_origin!.localRotation != worldRotationQuatnerion) { _origin.localRotation = worldRotationQuatnerion; } if (_origin.localPosition != positionVector) { _origin.localPosition = positionVector; } } } } } ================================================ FILE: NoodleExtensions/CutoutManager.cs ================================================ namespace NoodleExtensions { using System.Collections.Generic; using System.Reflection; using UnityEngine; internal static class CutoutManager { internal static Dictionary ObstacleCutoutEffects { get; } = new Dictionary(); internal static Dictionary NoteCutoutEffects { get; } = new Dictionary(); internal static Dictionary NoteDisappearingArrowWrappers { get; } = new Dictionary(); } internal abstract class CutoutWrapper { internal float Cutout { get; private set; } = 1; internal virtual void SetCutout(float cutout) { Cutout = cutout; } } internal class CutoutEffectWrapper : CutoutWrapper { private readonly CutoutEffect _cutoutEffect; internal CutoutEffectWrapper(CutoutEffect cutoutEffect) { _cutoutEffect = cutoutEffect; } internal override void SetCutout(float cutout) { base.SetCutout(cutout); _cutoutEffect.SetCutout(1 - cutout); } } internal class CutoutAnimateEffectWrapper : CutoutWrapper { private readonly CutoutAnimateEffect _cutoutAnimateEffect; internal CutoutAnimateEffectWrapper(CutoutAnimateEffect cutoutAnimateEffect) { _cutoutAnimateEffect = cutoutAnimateEffect; } internal override void SetCutout(float cutout) { base.SetCutout(cutout); _cutoutAnimateEffect.SetCutout(1 - cutout); } } internal class DisappearingArrowWrapper : CutoutWrapper { private readonly MonoBehaviour _disappearingArrowController; private readonly MethodInfo _method; internal DisappearingArrowWrapper(MonoBehaviour disappearingArrowController, MethodInfo method) { _disappearingArrowController = disappearingArrowController; _method = method; } internal override void SetCutout(float cutout) { base.SetCutout(cutout); // gross nasty reflection _method.Invoke(_disappearingArrowController, new object[] { cutout }); } } } ================================================ FILE: NoodleExtensions/Directory.Build.props ================================================ true true true false true true ================================================ FILE: NoodleExtensions/Directory.Build.targets ================================================  2.0 false $(OutputPath)$(AssemblyName) $(OutputPath)Final True $(BasePluginVersion) $(BasePluginVersion) $(BasePluginVersion) $(AssemblyName) $(ArtifactName)-$(PluginVersion) $(ArtifactName)-bs$(GameVersion) $(ArtifactName)-$(CommitHash) $(AssemblyName) $(AssemblyName) $(OutDir)zip\ $(BeatSaberDir)\Plugins True Unable to copy assembly to game folder, did you set 'BeatSaberDir' correctly in your 'csproj.user' file? Plugins folder doesn't exist: '$(PluginDir)'. Unable to copy to Plugins folder, '$(BeatSaberDir)' does not appear to be a Beat Saber game install. Unable to copy to Plugins folder, 'BeatSaberDir' has not been set in your 'csproj.user' file. False $(BeatSaberDir)\IPA\Pending\Plugins ================================================ FILE: NoodleExtensions/HarmonyPatches/BeatmapDataLoader.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using CustomJSONData.CustomBeatmap; using HarmonyLib; [HarmonyPatch(typeof(BeatmapDataLoader))] [HarmonyPatch("GetBeatmapDataFromBeatmapSaveData")] internal static class BeatmapDataLoaderGetBeatmapDataFromBeatmapSaveData { private static void Postfix(BeatmapData __result, float startBpm) { if (__result is CustomBeatmapData customBeatmapData) { foreach (BeatmapLineData beatmapLineData in customBeatmapData.beatmapLinesData) { foreach (BeatmapObjectData beatmapObjectData in beatmapLineData.beatmapObjectsData) { Dictionary dynData = beatmapObjectData.GetDataForObject(); // TODO: account for base game bpm changes // for per object njs and spawn offset float bpm = startBpm; dynData["bpm"] = bpm; } } } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/BeatmapDataTransformHelper.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Reflection.Emit; using CustomJSONData; using CustomJSONData.CustomBeatmap; using HarmonyLib; using Heck; using IPA.Utilities; using static NoodleExtensions.Plugin; [HeckPatch(typeof(BeatmapDataTransformHelper))] [HeckPatch("CreateTransformedBeatmapData")] internal static class BeatmapDataTransformHelperCreateTransformedBeatmapData { private static readonly MethodInfo _reorderLineData = AccessTools.Method(typeof(BeatmapDataTransformHelperCreateTransformedBeatmapData), nameof(ReorderLineData)); private static readonly FieldAccessor>.Accessor _beatmapObjectsDataAccessor = FieldAccessor>.GetAccessor("_beatmapObjectsData"); private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward( false, new CodeMatch(OpCodes.Ldloc_0), new CodeMatch(OpCodes.Ldarg_0), new CodeMatch(OpCodes.Bne_Un)) .Advance(1) .InsertAndAdvance( new CodeInstruction(OpCodes.Call, _reorderLineData), new CodeInstruction(OpCodes.Stloc_0), new CodeInstruction(OpCodes.Ldloc_0)) // Replace the opcode we replace .InstructionEnumeration(); } private static IReadonlyBeatmapData ReorderLineData(IReadonlyBeatmapData beatmapData) { if (beatmapData is CustomBeatmapData) { CustomBeatmapData customBeatmapData = (CustomBeatmapData)beatmapData.GetCopy(); // there is some ambiguity with these variables but who frikkin cares float startHalfJumpDurationInBeats = 4; float maxHalfJumpDistance = 18; float moveDuration = 0.5f; for (int i = 0; i < customBeatmapData.beatmapLinesData.Count; i++) { BeatmapLineData beatmapLineData = (BeatmapLineData)customBeatmapData.beatmapLinesData[i]; foreach (BeatmapObjectData beatmapObjectData in beatmapLineData.beatmapObjectsData) { Dictionary dynData = beatmapObjectData.GetDataForObject(); float noteJumpMovementSpeed = dynData.Get(NOTEJUMPSPEED) ?? GameplayCoreInstallerInstallBindings.CachedNoteJumpMovementSpeed; float noteJumpStartBeatOffset = dynData.Get(NOTESPAWNOFFSET) ?? GameplayCoreInstallerInstallBindings.CachedNoteJumpStartBeatOffset; // how do i not repeat this in a reasonable way float num = 60f / dynData.Get("bpm"); float num2 = startHalfJumpDurationInBeats; while (noteJumpMovementSpeed * num * num2 > maxHalfJumpDistance) { num2 /= 2f; } num2 += noteJumpStartBeatOffset; if (num2 < 1f) { num2 = 1f; } float jumpDuration = num * num2 * 2f; dynData["aheadTime"] = moveDuration + (jumpDuration * 0.5f); } _beatmapObjectsDataAccessor(ref beatmapLineData) = beatmapLineData.beatmapObjectsData .OrderBy(n => n.time - (float)(n.GetDataForObject()["aheadTime"] ?? throw new System.InvalidOperationException($"Could not get aheadTime for [{n.GetType().FullName}] at time [{n.time}]."))) .ToList(); } return customBeatmapData; } Logger.Log("beatmapData was not CustomBeatmapData", IPA.Logging.Logger.Level.Error); return beatmapData; } private static void Postfix(IReadonlyBeatmapData __result) { // Skip if calling class is MultiplayerConnectPlayerInstaller StackTrace stackTrace = new StackTrace(); if (!stackTrace.GetFrame(2).GetMethod().Name.Contains("MultiplayerConnectedPlayerInstaller")) { NoodleObjectDataManager.DeserializeBeatmapData(__result); Animation.NoodleEventDataManager.DeserializeBeatmapData(__result); } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/BeatmapObjectCallBackController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using CustomJSONData.CustomBeatmap; using HarmonyLib; using Heck; using static NoodleExtensions.NoodleObjectDataManager; [HeckPatch(typeof(BeatmapObjectCallbackController))] [HeckPatch("LateUpdate")] internal static class BeatmapObjectCallBackControllerLateUpdate { private static readonly FieldInfo _aheadTimeField = AccessTools.Field(typeof(BeatmapObjectCallbackData), nameof(BeatmapObjectCallbackData.aheadTime)); private static readonly MethodInfo _getAheadTime = AccessTools.Method(typeof(BeatmapObjectCallBackControllerLateUpdate), nameof(GetAheadTime)); private static readonly MethodInfo _beatmapObjectSpawnControllerCallback = AccessTools.Method(typeof(BeatmapObjectSpawnController), nameof(BeatmapObjectSpawnController.HandleBeatmapObjectCallback)); private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Ldfld, _aheadTimeField)) .Advance(-1) .InsertAndAdvance( new CodeInstruction(OpCodes.Ldloc_1), new CodeInstruction(OpCodes.Ldloc_3)) .Advance(2) .Insert(new CodeInstruction(OpCodes.Call, _getAheadTime)) .InstructionEnumeration(); } private static float GetAheadTime(BeatmapObjectCallbackData beatmapObjectCallbackData, BeatmapObjectData beatmapObjectData, float @default) { if (beatmapObjectCallbackData.callback.Method == _beatmapObjectSpawnControllerCallback && (beatmapObjectData is CustomObstacleData || beatmapObjectData is CustomNoteData)) { NoodleObjectData? noodleData = TryGetObjectData(beatmapObjectData); if (noodleData != null) { float? aheadTime = noodleData.AheadTimeInternal; if (aheadTime.HasValue) { return aheadTime.Value; } } } return @default; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/BeatmapObjectManager.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; using Heck.Animation; using static NoodleExtensions.NoodleObjectDataManager; [HeckPatch(typeof(BeatmapObjectManager))] [HeckPatch("HandleNoteControllerNoteDidFinishJump")] internal static class BeatmapObjectManagerHandleNoteControllerNoteDidFinishJump { private static void Prefix(NoteController noteController) { NoodleObjectData? noodleData = TryGetObjectData(noteController.noteData); if (noodleData?.Track != null) { foreach (Track track in noodleData.Track) { track.RemoveGameObject(noteController.gameObject); } } } } [HeckPatch(typeof(BeatmapObjectManager))] [HeckPatch("HandleObstacleFinishedMovement")] internal static class BeatmapObjectManagerHandleObstacleFinishedMovement { private static void Prefix(ObstacleController obstacleController) { NoodleObjectData? noodleData = TryGetObjectData(obstacleController.obstacleData); if (noodleData?.Track != null) { foreach (Track track in noodleData.Track) { track.RemoveGameObject(obstacleController.gameObject); } } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/BeatmapObjectSpawnController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; [HeckPatch(typeof(BeatmapObjectSpawnController))] [HeckPatch("Start")] internal static class BeatmapObjectSpawnControllerStart { internal static BasicBeatmapObjectManager? BeatmapObjectManager { get; private set; } private static void Postfix(IBeatmapObjectSpawner ____beatmapObjectSpawner, BeatmapObjectSpawnMovementData ____beatmapObjectSpawnMovementData) { if (____beatmapObjectSpawner is BasicBeatmapObjectManager basicBeatmapObjectManager) { BeatmapObjectManager = basicBeatmapObjectManager; SpawnDataHelper.InitBeatmapObjectSpawnController(____beatmapObjectSpawnMovementData); } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/BeatmapObjectSpawnMovementData.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; using UnityEngine; using static NoodleExtensions.HarmonyPatches.SpawnDataHelper; using static NoodleExtensions.HarmonyPatches.SpawnDataHelper.BeatmapObjectSpawnMovementDataVariables; using static NoodleExtensions.NoodleObjectDataManager; [HeckPatch(typeof(BeatmapObjectSpawnMovementData))] [HeckPatch("GetObstacleSpawnData")] internal static class BeatmapObjectSpawnMovementDataGetObstacleSpawnData { private static void Postfix(Vector3 ____centerPos, ObstacleData obstacleData, ref BeatmapObjectSpawnMovementData.ObstacleSpawnData __result) { NoodleObstacleData? noodleData = TryGetObjectData(obstacleData); if (noodleData == null) { return; } float? njs = noodleData.NJS; float? spawnoffset = noodleData.SpawnOffset; float? startX = noodleData.StartX; float? startY = noodleData.StartY; float? height = noodleData.Height; Vector3? finalNoteOffset = null; Vector3 moveStartPos = __result.moveStartPos; Vector3 moveEndPos = __result.moveEndPos; Vector3 jumpEndPos = __result.jumpEndPos; float obstacleHeight = __result.obstacleHeight; GetNoteJumpValues(njs, spawnoffset, out float jumpDuration, out float _, out Vector3 localMoveStartPos, out Vector3 localMoveEndPos, out Vector3 localJumpEndPos); // Actual wall stuff if (startX.HasValue || startY.HasValue || njs.HasValue || spawnoffset.HasValue) { // Ripped from base game Vector3 noteOffset = GetNoteOffset(obstacleData, startX, null); noteOffset.y = startY.HasValue ? VerticalObstaclePosY + (startY.GetValueOrDefault(0) * NoteLinesDistance) : ((obstacleData.obstacleType == ObstacleType.Top) ? (TopObstaclePosY + JumpOffsetY) : VerticalObstaclePosY); finalNoteOffset = noteOffset; moveStartPos = localMoveStartPos + noteOffset; moveEndPos = localMoveEndPos + noteOffset; jumpEndPos = localJumpEndPos + noteOffset; } if (height.HasValue) { obstacleHeight = height.Value * NoteLinesDistance; } __result = new BeatmapObjectSpawnMovementData.ObstacleSpawnData(moveStartPos, moveEndPos, jumpEndPos, obstacleHeight, __result.moveDuration, jumpDuration, __result.noteLinesDistance); if (!finalNoteOffset.HasValue) { Vector3 noteOffset = GetNoteOffset(obstacleData, startX, null); noteOffset.y = (obstacleData.obstacleType == ObstacleType.Top) ? (TopObstaclePosY + JumpOffsetY) : VerticalObstaclePosY; finalNoteOffset = noteOffset; } noodleData.NoteOffset = ____centerPos + finalNoteOffset.Value; float? width = noodleData.Width; noodleData.XOffset = ((width.GetValueOrDefault(obstacleData.lineIndex) / 2f) - 0.5f) * NoteLinesDistance; } } [HeckPatch(typeof(BeatmapObjectSpawnMovementData))] [HeckPatch("GetJumpingNoteSpawnData")] internal static class BeatmapObjectSpawnMovementDataGetJumpingNoteSpawnData { private static void Postfix(BeatmapObjectSpawnMovementData __instance, Vector3 ____centerPos, float ____jumpDuration, NoteData noteData, ref BeatmapObjectSpawnMovementData.NoteSpawnData __result) { NoodleNoteData? noodleData = TryGetObjectData(noteData); if (noodleData == null) { return; } float? flipLineIndex = noodleData.FlipLineIndexInternal; float? njs = noodleData.NJS; float? spawnoffset = noodleData.SpawnOffset; float? startlinelayer = noodleData.StartNoteLineLayerInternal; bool gravityOverride = noodleData.DisableGravity; float? startRow = noodleData.StartX; float? startHeight = noodleData.StartY; float jumpDuration = ____jumpDuration; ////Vector3 moveStartPos = __result.moveStartPos; ////Vector3 moveEndPos = __result.moveEndPos; ////Vector3 jumpEndPos = __result.jumpEndPos; float jumpGravity = __result.jumpGravity; Vector3 noteOffset = GetNoteOffset(noteData, startRow, startlinelayer ?? (float)noteData.beforeJumpNoteLineLayer); if (startRow.HasValue || startHeight.HasValue || flipLineIndex.HasValue || njs.HasValue || spawnoffset.HasValue || startlinelayer.HasValue || gravityOverride) { GetNoteJumpValues(njs, spawnoffset, out jumpDuration, out float localJumpDistance, out Vector3 localMoveStartPos, out Vector3 localMoveEndPos, out Vector3 localJumpEndPos); float localNoteJumpMovementSpeed = njs ?? NoteJumpMovementSpeed; float startLayerLineYPos = LineYPosForLineLayer(noteData, startlinelayer ?? (float)noteData.beforeJumpNoteLineLayer); float lineYPos = LineYPosForLineLayer(noteData, startHeight); // Magic numbers below found with linear regression y=mx+b using existing HighestJumpPosYForLineLayer values float highestJump = startHeight.HasValue ? (0.875f * lineYPos) + 0.639583f + JumpOffsetY : __instance.HighestJumpPosYForLineLayer(noteData.noteLineLayer); float GetJumpGravity(float lineYPos) => 2f * (highestJump - (gravityOverride ? lineYPos : startLayerLineYPos)) / Mathf.Pow(localJumpDistance / localNoteJumpMovementSpeed * 0.5f, 2f); jumpGravity = GetJumpGravity(startLayerLineYPos); float newJumpGravity = gravityOverride ? GetJumpGravity(lineYPos) : jumpGravity; Vector3 jumpEndPos = localJumpEndPos + noteOffset; // IsBasicNote() check is skipped so bombs can flip too Vector3 noteOffset2 = GetNoteOffset(noteData, flipLineIndex ?? startRow, gravityOverride ? startHeight : startlinelayer ?? (float)noteData.beforeJumpNoteLineLayer); Vector3 moveStartPos = localMoveStartPos + noteOffset2; Vector3 moveEndPos = localMoveEndPos + noteOffset2; __result = new BeatmapObjectSpawnMovementData.NoteSpawnData(moveStartPos, moveEndPos, jumpEndPos, newJumpGravity, __result.moveDuration, jumpDuration); } // DEFINITE POSITION IS WEIRD, OK? // fuck float num = jumpDuration * 0.5f; float startVerticalVelocity = jumpGravity * num; float yOffset = (startVerticalVelocity * num) - (jumpGravity * num * num * 0.5f); noodleData.NoteOffset = ____centerPos + noteOffset + new Vector3(0, yOffset, 0); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/BeatmapObjectsInTimeRowProcessor.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Linq; using CustomJSONData.CustomBeatmap; using HarmonyLib; using Heck; using static NoodleExtensions.Plugin; [HarmonyPatch(typeof(BeatmapObjectsInTimeRowProcessor))] [HarmonyPatch("ProcessAllNotesInTimeRow")] internal static class NotesInTimeRowProcessorProcessAllNotesInTimeRow { private static void Prefix(List notesInTimeRow) { if (notesInTimeRow.FirstOrDefault() is CustomNoteData) { List notesToSetFlip = new List(); Dictionary> notesInColumns = new Dictionary>(); for (int i = 0; i < notesInTimeRow.Count; i++) { CustomNoteData noteData = (CustomNoteData)notesInTimeRow[i]; Dictionary dynData = noteData.customData; IEnumerable? position = dynData.GetNullableFloats(POSITION); float lineIndex = position?.ElementAtOrDefault(0) ?? (noteData.lineIndex - 2); float lineLayer = position?.ElementAtOrDefault(1) ?? (float)noteData.noteLineLayer; if (!notesInColumns.TryGetValue(lineIndex, out List list)) { list = new List(1); notesInColumns.Add(lineIndex, list); } bool flag = false; for (int k = 0; k < list.Count; k++) { IEnumerable? listPosition = list[k].customData.GetNullableFloats(POSITION); float listLineLayer = listPosition?.ElementAtOrDefault(1) ?? (float)list[k].noteLineLayer; if (listLineLayer > lineLayer) { list.Insert(k, noteData); flag = true; break; } } if (!flag) { list.Add(noteData); } // Flippy stuff IEnumerable? flip = dynData.GetNullableFloats(FLIP); float? flipX = flip?.ElementAtOrDefault(0); float? flipY = flip?.ElementAtOrDefault(1); if (flipX.HasValue || flipY.HasValue) { if (flipX.HasValue) { dynData["flipLineIndex"] = flipX.Value; } if (flipY.HasValue) { dynData["flipYSide"] = flipY.Value; } } else if (!dynData.ContainsKey("flipYSide")) { notesToSetFlip.Add(noteData); } } foreach (KeyValuePair> keyValue in notesInColumns) { List list2 = keyValue.Value; for (int m = 0; m < list2.Count; m++) { list2[m].customData["startNoteLineLayer"] = m; } } // Process flip data notesToSetFlip.ForEach(c => c.customData["flipYSide"] = 0); } } } [HarmonyPatch(typeof(BeatmapObjectsInTimeRowProcessor))] [HarmonyPatch("ProcessColorNotesInTimeRow")] internal static class NotesInTimeRowProcessorProcessColorNotesInTimeRow { private static void Prefix(List colorNotesData) { if (colorNotesData.FirstOrDefault() is CustomNoteData) { int customNoteCount = colorNotesData.Count; if (customNoteCount == 2) { float[] lineIndexes = new float[2]; float[] lineLayers = new float[2]; for (int i = 0; i < customNoteCount; i++) { Dictionary dynData = ((CustomNoteData)colorNotesData[i]).customData; IEnumerable? position = dynData.GetNullableFloats(POSITION); lineIndexes[i] = position?.ElementAtOrDefault(0) ?? (colorNotesData[i].lineIndex - 2); lineLayers[i] = position?.ElementAtOrDefault(1) ?? (float)colorNotesData[i].noteLineLayer; } if (colorNotesData[0].colorType != colorNotesData[1].colorType && ((colorNotesData[0].colorType == ColorType.ColorA && lineIndexes[0] > lineIndexes[1]) || (colorNotesData[0].colorType == ColorType.ColorB && lineIndexes[0] < lineIndexes[1]))) { for (int i = 0; i < customNoteCount; i++) { // apparently I can use customData to store my own variables in noteData, neat // ^ comment from a very young and naive aero Dictionary dynData = ((CustomNoteData)colorNotesData[i]).customData; dynData["flipLineIndex"] = lineIndexes[1 - i]; float flipYSide = (lineIndexes[i] > lineIndexes[1 - i]) ? 1 : -1; if ((lineIndexes[i] > lineIndexes[1 - i] && lineLayers[i] < lineLayers[1 - i]) || (lineIndexes[i] < lineIndexes[1 - i] && lineLayers[i] > lineLayers[1 - i])) { flipYSide *= -1f; } dynData["flipYSide"] = flipYSide; } } } } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/Cutout/BaseNoteVisual.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Heck; using IPA.Utilities; using UnityEngine; [HeckPatch(typeof(BaseNoteVisuals))] [HeckPatch("Awake")] internal static class BaseNoteVisualsAwake { private static readonly Dictionary _setArrowTransparencyMethods = new Dictionary(); private static readonly FieldAccessor.Accessor _noteCutoutAnimateEffectAccessor = FieldAccessor.GetAccessor("_cutoutAnimateEffect"); private static readonly FieldAccessor.Accessor _cutoutEffectAccessor = FieldAccessor.GetAccessor("_cuttoutEffects"); private static void Postfix(BaseNoteVisuals __instance, NoteControllerBase ____noteController) { GameObject gameObject = __instance.gameObject; BaseNoteVisuals baseNoteVisuals = gameObject.GetComponent(); CutoutAnimateEffect cutoutAnimateEffect = _noteCutoutAnimateEffectAccessor(ref baseNoteVisuals); CutoutEffect[] cutoutEffects = _cutoutEffectAccessor(ref cutoutAnimateEffect); CutoutEffect cutoutEffect = cutoutEffects.First(n => n.name != "NoteArrow"); // 1.11 NoteArrow has been added to the CutoutAnimateEffect and we don't want that CutoutManager.NoteCutoutEffects.Add(____noteController, new CutoutEffectWrapper(cutoutEffect)); if (____noteController is ICubeNoteTypeProvider) { Type constructed = typeof(DisappearingArrowControllerBase<>).MakeGenericType(____noteController.GetType()); MonoBehaviour disappearingArrowController = (MonoBehaviour)gameObject.GetComponent(constructed); MethodInfo method = GetSetArrowTransparency(disappearingArrowController.GetType()); DisappearingArrowWrapper wrapper = new DisappearingArrowWrapper(disappearingArrowController, method); wrapper.SetCutout(1); // i have no fucking idea how this fixes the weird ghost arrow bug CutoutManager.NoteDisappearingArrowWrappers.Add(____noteController, wrapper); } } private static MethodInfo GetSetArrowTransparency(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type)); } if (_setArrowTransparencyMethods.TryGetValue(type, out MethodInfo value)) { return value; } Type baseType = type.BaseType; ////NoodleLogger.IPAlogger.Debug($"Base type is {baseType.Name}<{string.Join(", ", baseType.GenericTypeArguments.Select(t => t.Name))}>"); MethodInfo method = baseType.GetMethod("SetArrowTransparency", BindingFlags.NonPublic | BindingFlags.Instance); _setArrowTransparencyMethods[type] = method ?? throw new InvalidOperationException($"Type [{type.FullName}] does not contain method [SetArrowTransparency]"); return method; } } [HeckPatch(typeof(BaseNoteVisuals))] [HeckPatch("OnDestroy")] internal static class DisappearingArrowControllerBaseGameNoteControllerOnDestroy { private static void Postfix(NoteControllerBase ____noteController) { CutoutManager.NoteCutoutEffects.Remove(____noteController); CutoutManager.NoteDisappearingArrowWrappers.Remove(____noteController); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/Cutout/ObstacleDissolve.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; [HeckPatch(typeof(ObstacleDissolve))] [HeckPatch("Awake")] internal static class ObstacleDissolveAwake { private static void Postfix(ObstacleController ____obstacleController, CutoutAnimateEffect ____cutoutAnimateEffect) { CutoutManager.ObstacleCutoutEffects.Add(____obstacleController, new CutoutAnimateEffectWrapper(____cutoutAnimateEffect)); } } [HeckPatch(typeof(ObstacleDissolve))] [HeckPatch("OnDestroy")] internal static class ObstacleDissolveOnDestroy { private static void Postfix(ObstacleController ____obstacleController) { CutoutManager.ObstacleCutoutEffects.Remove(____obstacleController); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/BadNoteCutEffectSpawner.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; [HeckPatch(typeof(BadNoteCutEffectSpawner))] [HeckPatch("HandleNoteWasCut")] internal static class BadNoteCutEffectSpawnerHandleNoteWasCut { private static bool Prefix(NoteController noteController) { return FakeNoteHelper.GetFakeNote(noteController); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/BeatmapData.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using CustomJSONData; using CustomJSONData.CustomBeatmap; using HarmonyLib; [HarmonyPatch(typeof(BeatmapData))] [HarmonyPatch(MethodType.Constructor)] [HarmonyPatch(new Type[] { typeof(int) })] internal static class BeatmapDataCtor { private static void Postfix() { BeatmapDataAddBeatmapObjectData.NeedsCheck = true; } } [HarmonyPatch(typeof(BeatmapData))] [HarmonyPatch("AddBeatmapObjectData")] internal static class BeatmapDataAddBeatmapObjectData { private static readonly List _countGetters = new List { AccessTools.PropertyGetter(typeof(BeatmapData), nameof(BeatmapData.obstaclesCount)), AccessTools.PropertyGetter(typeof(BeatmapData), nameof(BeatmapData.cuttableNotesCount)), AccessTools.PropertyGetter(typeof(BeatmapData), nameof(BeatmapData.bombsCount)), }; private static readonly MethodInfo _fakeObjectCheck = AccessTools.Method(typeof(BeatmapDataAddBeatmapObjectData), nameof(FakeObjectCheck)); private static bool _noodleRequirement; internal static bool NeedsCheck { get; set; } private static void Prefix(BeatmapData __instance) { if (NeedsCheck) { NeedsCheck = false; if (__instance is CustomBeatmapData customBeatmapData) { IEnumerable? requirements = customBeatmapData.beatmapCustomData.Get>("_requirements")?.Cast(); _noodleRequirement = requirements?.Contains(Plugin.CAPABILITY) ?? false; } else { _noodleRequirement = false; } } } private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Call) { operands = _countGetters }) .Repeat(n => n .InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_0)) .Advance(1) .InsertAndAdvance( new CodeInstruction(OpCodes.Ldarg_1), new CodeInstruction(OpCodes.Call, _fakeObjectCheck)) .RemoveInstructions(5)) .InstructionEnumeration(); } private static int FakeObjectCheck(int objectCount, BeatmapObjectData beatmapObjectData) { if (_noodleRequirement) { bool? fake = beatmapObjectData.GetDataForObject().Get(Plugin.FAKENOTE); if (fake.HasValue && fake.Value) { return objectCount; } } return objectCount + 1; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/BeatmapObjectExecutionRatingsRecorder.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using HarmonyLib; using Heck; [HeckPatch(typeof(BeatmapObjectExecutionRatingsRecorder))] [HeckPatch("Update")] internal static class BeatmapObjectExecutionRatingsRecorderUpdate { private static IEnumerable Transpiler(IEnumerable instructions) { return FakeNoteHelper.ObstaclesTranspiler(instructions); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/BeatmapObjectManager.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using System.Reflection; using HarmonyLib; using Heck; [HeckPatch(typeof(BeatmapObjectManager))] [HeckPatch("HandleNoteControllerNoteWasCut")] internal static class BeatmapObjectManagerHandleNoteWasCut { private static readonly MethodInfo _despawnMethod = AccessTools.Method(typeof(BeatmapObjectManager), "Despawn", new Type[] { typeof(NoteController) }); [HarmonyPriority(Priority.High)] private static bool Prefix(BeatmapObjectManager __instance, NoteController noteController, in NoteCutInfo noteCutInfo) { if (!FakeNoteHelper.GetFakeNote(noteController)) { NoteCutCoreEffectsSpawnerStart.NoteCutCoreEffectsSpawner!.HandleNoteWasCut(noteController, noteCutInfo); _despawnMethod.Invoke(__instance, new object[] { noteController }); return false; } return true; } } [HeckPatch(typeof(BeatmapObjectManager))] [HeckPatch("HandleNoteControllerNoteWasMissed")] internal static class BeatmapObjectManagerHandleNoteWasMissed { [HarmonyPriority(Priority.High)] private static bool Prefix(NoteController noteController) { return FakeNoteHelper.GetFakeNote(noteController); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/BombCutSoundEffectManager.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; [HeckPatch(typeof(BombCutSoundEffectManager))] [HeckPatch("HandleNoteWasCut")] internal static class BombCutSoundEffectManagerHandleNoteWasCut { // Do not create a BombCutSoundEffect for fake notes private static bool Prefix(NoteController noteController) { return FakeNoteHelper.GetFakeNote(noteController); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/BombNoteController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using HarmonyLib; using Heck; [HeckPatch(typeof(BombNoteController))] [HeckPatch("Init")] internal static class BombNoteControllerInit { [HarmonyPriority(Priority.High)] private static void Postfix(NoteData noteData, CuttableBySaber ____cuttableBySaber) { if (!FakeNoteHelper.GetCuttable(noteData)) { ____cuttableBySaber.canBeCut = false; } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/FakeNoteHelper.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using UnityEngine; using static NoodleExtensions.NoodleObjectDataManager; internal static class FakeNoteHelper { internal static readonly MethodInfo _boundsNullCheck = AccessTools.Method(typeof(FakeNoteHelper), nameof(BoundsNullCheck)); private static readonly MethodInfo _intersectingObstaclesGetter = AccessTools.PropertyGetter(typeof(PlayerHeadAndObstacleInteraction), nameof(PlayerHeadAndObstacleInteraction.intersectingObstacles)); private static readonly MethodInfo _obstacleFakeCheck = AccessTools.Method(typeof(FakeNoteHelper), nameof(ObstacleFakeCheck)); internal static bool GetFakeNote(NoteController noteController) { NoodleNoteData? noodleData = TryGetObjectData(noteController.noteData); if (noodleData != null) { bool? fake = noodleData.Fake; if (fake.HasValue && fake.Value) { return false; } } return true; } internal static bool GetCuttable(NoteData noteData) { NoodleNoteData? noodleData = TryGetObjectData(noteData); if (noodleData != null) { bool? cuttable = noodleData.Cuttable; if (cuttable.HasValue && !cuttable.Value) { return false; } } return true; } internal static IEnumerable ObstaclesTranspiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Callvirt, _intersectingObstaclesGetter)) .Advance(1) .Insert(new CodeInstruction(OpCodes.Call, _obstacleFakeCheck)) .InstructionEnumeration(); } private static bool BoundsNullCheck(ObstacleController obstacleController) { return obstacleController.bounds.size == Vector3.zero; } private static List ObstacleFakeCheck(List intersectingObstacles) { return intersectingObstacles.Where(n => { NoodleObstacleData? noodleData = TryGetObjectData(n.obstacleData); if (noodleData != null) { bool? fake = noodleData.Fake; if (fake.HasValue && fake.Value) { return false; } } return true; }).ToList(); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/GameEnergyCounter.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using HarmonyLib; using Heck; [HeckPatch(typeof(GameEnergyCounter))] [HeckPatch("LateUpdate")] internal static class GameEnergyCounterUpdate { private static IEnumerable Transpiler(IEnumerable instructions) { return FakeNoteHelper.ObstaclesTranspiler(instructions); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/GameNoteController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using HarmonyLib; using Heck; [HeckPatch(typeof(GameNoteController))] [HeckPatch("NoteDidStartJump")] internal static class GameNoteControllerNoteDidStartJump { [HarmonyPriority(Priority.High)] private static bool Prefix(GameNoteController __instance) { return FakeNoteHelper.GetCuttable(__instance.noteData); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/NoteCutCoreEffectSpawner.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; [HeckPatch(typeof(NoteCutCoreEffectsSpawner))] [HeckPatch("Start")] internal static class NoteCutCoreEffectsSpawnerStart { internal static NoteCutCoreEffectsSpawner? NoteCutCoreEffectsSpawner { get; private set; } private static void Postfix(NoteCutCoreEffectsSpawner __instance) { NoteCutCoreEffectsSpawner = __instance; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/NoteCutScoreSpawner.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; [HeckPatch(typeof(NoteCutScoreSpawner))] [HeckPatch("HandleNoteWasCut")] internal static class NoteCutScoreSpawnerHandleNoteWasCut { private static bool Prefix(NoteController noteController) { return FakeNoteHelper.GetFakeNote(noteController); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/NoteCutSoundEffectManager.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using Heck; using UnityEngine; [HeckPatch(typeof(NoteCutSoundEffectManager))] [HeckPatch("Start")] internal static class NoteCutSoundEffectManagerStart { private static void Postfix(NoteCutSoundEffectManager __instance) { NoodleCutSoundEffectManager noodleManager = __instance.gameObject.AddComponent(); noodleManager.Init(__instance); NoteCutSoundEffectManagerHandleNoteWasSpawned.NoodleManager = noodleManager; } } [HeckPatch(typeof(NoteCutSoundEffectManager))] [HeckPatch("HandleNoteWasSpawned")] internal static class NoteCutSoundEffectManagerHandleNoteWasSpawned { internal static NoodleCutSoundEffectManager? NoodleManager { get; set; } // Do not create a NoteCutSoundEffect for fake notes private static bool Prefix(NoteController noteController) { if (FakeNoteHelper.GetFakeNote(noteController)) { return NoodleManager!.ProcessHitSound(noteController); } return false; } } // Weird cut sound shenanigans to prevent unity from crashing. internal class NoodleCutSoundEffectManager : MonoBehaviour { private readonly List _hitsoundQueue = new List(); private NoteCutSoundEffectManager? _noteCutSoundEffectManager; private int _lastFrame = -1; private int _cutCount = -1; internal void Init(NoteCutSoundEffectManager noteCutSoundEffectManager) { _noteCutSoundEffectManager = noteCutSoundEffectManager; } internal bool ProcessHitSound(NoteController noteController) { if (Time.frameCount == _lastFrame) { _cutCount++; } else { _lastFrame = Time.frameCount; _cutCount = 1; } // We do not allow more than 30 NoteCutSoundEffects to be created in a single frame to prevent unity from dying if (_cutCount < 30) { return true; } else { _hitsoundQueue.Add(noteController); return false; } } private void Update() { if (_hitsoundQueue.Count > 0 && Time.frameCount != _lastFrame) { List noteControllers = new List(_hitsoundQueue); _hitsoundQueue.Clear(); noteControllers.ForEach(_noteCutSoundEffectManager!.HandleNoteWasSpawned); Plugin.Logger.Log($"{noteControllers.Count} cut sounds moved to next frame!"); } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/ObstacleSaberSparkleEffectManager.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; [HeckPatch(typeof(ObstacleSaberSparkleEffectManager))] [HeckPatch("Update")] internal static class ObstacleSaberSparkleEffectManagerUpdate { private static readonly MethodInfo _currentGetter = AccessTools.PropertyGetter(typeof(List.Enumerator), nameof(List.Enumerator.Current)); private static IEnumerable Transpiler(IEnumerable instructions) { CodeMatcher codeMatcher = new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Br)); object label = codeMatcher.Operand; return codeMatcher .MatchForward(false, new CodeMatch(OpCodes.Call, _currentGetter)) .Advance(2) .Insert( new CodeInstruction(OpCodes.Ldloc_1), new CodeInstruction(OpCodes.Call, FakeNoteHelper._boundsNullCheck), new CodeInstruction(OpCodes.Brtrue_S, label)) .InstructionEnumeration(); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/FakeNotes/PlayerHeadAndObstacleInteraction.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; [HeckPatch(typeof(PlayerHeadAndObstacleInteraction))] [HeckPatch("GetObstaclesContainingPoint")] internal static class PlayerHeadAndObstacleInteractionGetObstaclesContainingPoint { private static readonly MethodInfo _currentGetter = AccessTools.PropertyGetter(typeof(List.Enumerator), nameof(List.Enumerator.Current)); private static IEnumerable Transpiler(IEnumerable instructions) { CodeMatcher codeMatcher = new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Br)); object label = codeMatcher.Operand; return codeMatcher .MatchForward(false, new CodeMatch(OpCodes.Call, _currentGetter)) .Advance(2) .Insert( new CodeInstruction(OpCodes.Ldloc_1), new CodeInstruction(OpCodes.Call, FakeNoteHelper._boundsNullCheck), new CodeInstruction(OpCodes.Brtrue_S, label)) .InstructionEnumeration(); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/GameplayCoreInstaller.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; [HeckPatch(typeof(GameplayCoreInstaller))] [HeckPatch("InstallBindings")] internal static class GameplayCoreInstallerInstallBindings { private static readonly MethodInfo _createTransformedBeatmapData = AccessTools.Method(typeof(BeatmapDataTransformHelper), nameof(BeatmapDataTransformHelper.CreateTransformedBeatmapData)); private static readonly MethodInfo _cacheNoteJumpValues = AccessTools.Method(typeof(GameplayCoreInstallerInstallBindings), nameof(CacheNoteJumpValues)); internal static float CachedNoteJumpMovementSpeed { get; private set; } internal static float CachedNoteJumpStartBeatOffset { get; private set; } private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Call, _createTransformedBeatmapData)) .Insert( new CodeInstruction(OpCodes.Ldloc_S, 9), new CodeInstruction(OpCodes.Ldloc_S, 10), new CodeInstruction(OpCodes.Call, _cacheNoteJumpValues)) .InstructionEnumeration(); } private static void CacheNoteJumpValues(float defaultNoteJumpMovementSpeed, float defaultNoteJumpStartBeatOffset) { CachedNoteJumpMovementSpeed = defaultNoteJumpMovementSpeed; CachedNoteJumpStartBeatOffset = defaultNoteJumpStartBeatOffset; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/LeftHanded/BeatmapDataMirrorTransform.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using CustomJSONData; using CustomJSONData.CustomBeatmap; using HarmonyLib; using static NoodleExtensions.Plugin; [HarmonyPatch(typeof(BeatmapDataMirrorTransform))] [HarmonyPatch("CreateTransformedData")] internal static class BeatDataMirrorTransformCreateTransformedData { private static void Postfix(IReadonlyBeatmapData __result) { foreach (BeatmapEventData beatmapEventData in __result.beatmapEventsData) { if (beatmapEventData.type.IsRotationEvent()) { if (beatmapEventData is CustomBeatmapEventData customData) { Dictionary dynData = customData.customData; float? rotation = dynData.Get(ROTATION); if (rotation.HasValue) { dynData["_rotation"] = rotation * -1; } } } } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/LeftHanded/NoteData.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using System.Collections.Generic; using System.Linq; using CustomJSONData; using CustomJSONData.CustomBeatmap; using HarmonyLib; using Heck; using UnityEngine; using static NoodleExtensions.Plugin; [HarmonyPatch(typeof(NoteData))] [HarmonyPatch("Mirror")] internal static class NoteDataMirror { private static void Postfix(NoteData __instance) { if (__instance is CustomNoteData customData) { Dictionary dynData = customData.customData; IEnumerable? position = dynData.GetNullableFloats(POSITION); float? flipLineIndex = dynData.Get("flipLineIndex"); IEnumerable? flip = dynData.GetNullableFloats(FLIP); List? localrot = dynData.Get>(LOCALROTATION)?.Select(n => Convert.ToSingle(n)).ToList(); object? rotation = dynData.Get(ROTATION); float? startRow = position?.ElementAtOrDefault(0); float? flipX = flip?.ElementAtOrDefault(0); if (startRow.HasValue) { dynData[POSITION] = new List() { ((startRow.Value + 0.5f) * -1) - 0.5f, position.ElementAtOrDefault(1) }; } if (flipLineIndex.HasValue) { dynData["flipLineIndex"] = ((flipLineIndex.Value + 0.5f) * -1) - 0.5f; } if (flipX.HasValue) { dynData[FLIP] = new List() { ((flipX.Value + 0.5f) * -1) - 0.5f, flip.ElementAtOrDefault(1) }; } if (localrot != null) { List rot = localrot.Select(n => Convert.ToSingle(n)).ToList(); Quaternion modifiedVector = Quaternion.Euler(rot[0], rot[1], rot[2]); Vector3 vector = new Quaternion(modifiedVector.x, modifiedVector.y * -1, modifiedVector.z * -1, modifiedVector.w).eulerAngles; dynData[LOCALROTATION] = new List { vector.x, vector.y, vector.z }; } if (rotation != null) { if (rotation is List list) { List rot = list.Select(n => Convert.ToSingle(n)).ToList(); Quaternion modifiedVector = Quaternion.Euler(rot[0], rot[1], rot[2]); Vector3 vector = new Quaternion(modifiedVector.x, modifiedVector.y * -1, modifiedVector.z * -1, modifiedVector.w).eulerAngles; dynData[ROTATION] = new List { vector.x, vector.y, vector.z }; } else { dynData[ROTATION] = Convert.ToSingle(rotation) * -1; } } float? cutDirection = dynData.Get(CUTDIRECTION); if (cutDirection.HasValue) { dynData[CUTDIRECTION] = 360 - cutDirection.Value; } } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/LeftHanded/ObstacleData.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using System.Collections.Generic; using System.Linq; using CustomJSONData; using CustomJSONData.CustomBeatmap; using HarmonyLib; using Heck; using UnityEngine; using static NoodleExtensions.Plugin; [HarmonyPatch(typeof(ObstacleData))] [HarmonyPatch("Mirror")] internal static class ObstacleDataMirror { private static void Prefix(ObstacleData __instance) // prefix because we need to know the lineIndex before it gets mirrored { if (__instance is CustomObstacleData customData) { Dictionary dynData = customData.customData; IEnumerable? position = dynData.GetNullableFloats(POSITION); IEnumerable? scale = dynData.GetNullableFloats(SCALE); List? localrot = dynData.Get>(LOCALROTATION)?.Select(n => Convert.ToSingle(n)).ToList(); object? rotation = dynData.Get(ROTATION); float? startX = position?.ElementAtOrDefault(0); float? scaleX = scale?.ElementAtOrDefault(0); float width = scaleX.GetValueOrDefault(__instance.width); if (startX.HasValue) { dynData[POSITION] = new List() { (startX.Value + width) * -1, position.ElementAtOrDefault(1) }; } else if (scaleX.HasValue) { float lineIndex = __instance.lineIndex - 2; dynData[POSITION] = new List() { (lineIndex + width) * -1, position?.ElementAtOrDefault(1) ?? 0 }; } if (localrot != null) { List rot = localrot.Select(n => Convert.ToSingle(n)).ToList(); Quaternion modifiedVector = Quaternion.Euler(rot[0], rot[1], rot[2]); Vector3 vector = new Quaternion(modifiedVector.x, modifiedVector.y * -1, modifiedVector.z * -1, modifiedVector.w).eulerAngles; dynData[LOCALROTATION] = new List { vector.x, vector.y, vector.z }; } if (rotation != null) { if (rotation is List list) { List rot = list.Select(n => Convert.ToSingle(n)).ToList(); Quaternion modifiedVector = Quaternion.Euler(rot[0], rot[1], rot[2]); Vector3 vector = new Quaternion(modifiedVector.x, modifiedVector.y * -1, modifiedVector.z * -1, modifiedVector.w).eulerAngles; dynData[ROTATION] = new List { vector.x, vector.y, vector.z }; } else { dynData[ROTATION] = Convert.ToSingle(rotation) * -1; } } } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/Mirror/MirroredNoteController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using Heck; using Heck.Animation; using UnityEngine; using static NoodleExtensions.NoodleObjectDataManager; internal static class MirroredNoteControllerHelper { internal static void AddToTrack(NoteData noteData, GameObject gameObject) { NoodleNoteData? noodleData = TryGetObjectData(noteData); if (noodleData != null) { IEnumerable? tracks = noodleData.Track; if (tracks != null) { foreach (Track track in tracks) { // add to gameobjects track.AddGameObject(gameObject); } } } } internal static bool CheckSkip(Transform noteTransform, Transform followedNoteTransform) { if (followedNoteTransform.position.y < 0) { if (noteTransform.gameObject.activeInHierarchy) { noteTransform.gameObject.SetActive(false); } return false; } if (!noteTransform.gameObject.activeInHierarchy) { noteTransform.gameObject.SetActive(true); } return true; } internal static void UpdateMirror(Transform objectTransform, Transform noteTransform, Transform followedObjectTransform, Transform followedNoteTransform, NoteControllerBase noteController, NoteControllerBase followedNote) { if (objectTransform.localScale != followedObjectTransform.localScale) { objectTransform.localScale = followedObjectTransform.localScale; } if (noteTransform.localScale != followedNoteTransform.localScale) { noteTransform.localScale = followedNoteTransform.localScale; } if (CutoutManager.NoteCutoutEffects.TryGetValue(noteController, out CutoutEffectWrapper cutoutEffect)) { if (CutoutManager.NoteCutoutEffects.TryGetValue(followedNote, out CutoutEffectWrapper followedCutoutEffect)) { cutoutEffect.SetCutout(followedCutoutEffect.Cutout); } } if (CutoutManager.NoteDisappearingArrowWrappers.TryGetValue(noteController, out DisappearingArrowWrapper disappearingArrow)) { if (CutoutManager.NoteDisappearingArrowWrappers.TryGetValue(followedNote, out DisappearingArrowWrapper followedDisappearingArrow)) { disappearingArrow.SetCutout(followedDisappearingArrow.Cutout); } } } } // Fuck generics [HeckPatch(typeof(MirroredNoteController))] [HeckPatch("Mirror")] internal static class MirroredNoteControllerINoteMirrorableMirror { private static void Postfix(MirroredNoteController __instance) { MirroredNoteControllerHelper.AddToTrack(__instance.noteData, __instance.gameObject); } } [HeckPatch(typeof(MirroredNoteController))] [HeckPatch("Mirror")] internal static class MirroredNoteControllerICubeNoteMirrorableMirror { private static void Postfix(MirroredNoteController __instance) { MirroredNoteControllerHelper.AddToTrack(__instance.noteData, __instance.gameObject); } } [HeckPatch(typeof(MirroredNoteController))] [HeckPatch("UpdatePositionAndRotation")] internal static class MirroredNoteControllerINoteMirrorableUpdatePositionAndRotation { private static bool Prefix(Transform ____noteTransform, Transform ____followedNoteTransform) { return MirroredNoteControllerHelper.CheckSkip(____noteTransform, ____followedNoteTransform); } private static void Postfix(MirroredNoteController __instance, INoteMirrorable ___followedNote, Transform ____objectTransform, Transform ____noteTransform, Transform ____followedObjectTransform, Transform ____followedNoteTransform) { MirroredNoteControllerHelper.UpdateMirror(____objectTransform, ____noteTransform, ____followedObjectTransform, ____followedNoteTransform, __instance, (NoteControllerBase)___followedNote); } } [HeckPatch(typeof(MirroredNoteController))] [HeckPatch("UpdatePositionAndRotation")] internal static class MirroredNoteControllerICubeNoteMirrorableUpdatePositionAndRotation { private static bool Prefix(Transform ____noteTransform, Transform ____followedNoteTransform) { return MirroredNoteControllerHelper.CheckSkip(____noteTransform, ____followedNoteTransform); } private static void Postfix(MirroredNoteController __instance, ICubeNoteMirrorable ___followedNote, Transform ____objectTransform, Transform ____noteTransform, Transform ____followedObjectTransform, Transform ____followedNoteTransform) { MirroredNoteControllerHelper.UpdateMirror(____objectTransform, ____noteTransform, ____followedObjectTransform, ____followedNoteTransform, __instance, (NoteControllerBase)___followedNote); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/Mirror/MirroredObstacleController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; using UnityEngine; [HeckPatch(typeof(MirroredObstacleController))] [HeckPatch("UpdatePositionAndRotation")] internal static class MirroredObstacleControllerUpdatePositionAndRotation { // Must be overwritten to compensate for rotation private static bool Prefix(MirroredObstacleController __instance, ObstacleController ____followedObstacle, Transform ____transform, Transform ____followedTransform) { // do not reflection walls above the mirror if (____followedTransform.position.y < 0) { // idk how to hide it without disabling the update ____transform.position = new Vector3(0, 100, 0); return false; } Vector3 position = ____followedTransform.position; Quaternion quaternion = ____followedTransform.rotation; position.y = -position.y; quaternion = quaternion.Reflect(Vector3.up); ____transform.SetPositionAndRotation(position, quaternion); if (____transform.localScale != ____followedTransform.localScale) { ____transform.localScale = ____followedTransform.localScale; } if (CutoutManager.ObstacleCutoutEffects.TryGetValue(__instance, out CutoutAnimateEffectWrapper cutoutAnimateEffect)) { if (CutoutManager.ObstacleCutoutEffects.TryGetValue(____followedObstacle, out CutoutAnimateEffectWrapper followedCutoutAnimateEffect)) { cutoutAnimateEffect.SetCutout(followedCutoutAnimateEffect.Cutout); } } return false; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/MultiplayerConnectedPlayerInstaller.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using CustomJSONData; using CustomJSONData.CustomBeatmap; using HarmonyLib; using Heck; using IPA.Utilities; using static NoodleExtensions.Plugin; [HeckPatch(typeof(MultiplayerConnectedPlayerInstaller))] [HeckPatch("InstallBindings")] internal static class MultiplayerConnectedPlayerInstallerInstallBindings { private static readonly MethodInfo _createTransformedBeatmapData = AccessTools.Method(typeof(BeatmapDataTransformHelper), nameof(BeatmapDataTransformHelper.CreateTransformedBeatmapData)); private static readonly MethodInfo _excludeFakeNote = AccessTools.Method(typeof(MultiplayerConnectedPlayerInstallerInstallBindings), nameof(ExcludeFakeNoteAndAllWalls)); private static readonly FieldAccessor>.Accessor _beatmapObjectsDataAccessor = FieldAccessor>.GetAccessor("_beatmapObjectsData"); private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Call, _createTransformedBeatmapData)) .Advance(1) .Insert(new CodeInstruction(OpCodes.Call, _excludeFakeNote)) .InstructionEnumeration(); } private static IReadonlyBeatmapData ExcludeFakeNoteAndAllWalls(IReadonlyBeatmapData result) { foreach (BeatmapLineData b in result.beatmapLinesData) { BeatmapLineData refBeatmapLineData = b; _beatmapObjectsDataAccessor(ref refBeatmapLineData) = b.beatmapObjectsData.Where(n => { Dictionary dynData; switch (n) { case CustomNoteData customNoteData: dynData = customNoteData.customData; break; case CustomObstacleData customObstacleData: return false; default: return true; } bool? fake = dynData.Get(FAKENOTE); if (fake.HasValue && fake.Value) { return false; } return true; }).ToList(); } if (result is CustomBeatmapData customBeatmapData) { string[] excludedTypes = new string[] { ASSIGNPLAYERTOTRACK, ASSIGNTRACKPARENT, }; customBeatmapData.customEventsData.RemoveAll(n => excludedTypes.Contains(n.type)); } return result; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/NoteController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; using Heck.Animation; using IPA.Utilities; using UnityEngine; using static NoodleExtensions.NoodleObjectDataManager; [HeckPatch(typeof(NoteController))] [HeckPatch("Init")] internal static class NoteControllerInit { internal static readonly FieldAccessor.Accessor _noteJumpAccessor = FieldAccessor.GetAccessor("_jump"); internal static readonly FieldAccessor.Accessor _noteFloorMovementAccessor = FieldAccessor.GetAccessor("_floorMovement"); internal static readonly FieldAccessor.Accessor _worldRotationFloorAccessor = FieldAccessor.GetAccessor("_worldRotation"); internal static readonly FieldAccessor.Accessor _inverseWorldRotationFloorAccessor = FieldAccessor.GetAccessor("_inverseWorldRotation"); internal static readonly FieldAccessor.Accessor _worldRotationJumpAccessor = FieldAccessor.GetAccessor("_worldRotation"); internal static readonly FieldAccessor.Accessor _inverseWorldRotationJumpAccessor = FieldAccessor.GetAccessor("_inverseWorldRotation"); private static readonly FieldAccessor.Accessor _endRotationAccessor = FieldAccessor.GetAccessor("_endRotation"); private static readonly FieldAccessor.Accessor _middleRotationAccessor = FieldAccessor.GetAccessor("_middleRotation"); private static readonly FieldAccessor.Accessor _randomRotationsAccessor = FieldAccessor.GetAccessor("_randomRotations"); private static readonly FieldAccessor.Accessor _randomRotationIdxAccessor = FieldAccessor.GetAccessor("_randomRotationIdx"); private static readonly FieldInfo _noteDataField = AccessTools.Field(typeof(NoteController), "_noteData"); private static readonly MethodInfo _flipYSideGetter = AccessTools.PropertyGetter(typeof(NoteData), nameof(NoteData.flipYSide)); private static readonly MethodInfo _getFlipYSide = AccessTools.Method(typeof(NoteControllerInit), nameof(GetFlipYSide)); private static void Postfix(NoteController __instance, NoteData noteData, NoteMovement ____noteMovement, Vector3 moveStartPos, Vector3 moveEndPos, Vector3 jumpEndPos, float endRotation) { NoodleNoteData? noodleData = TryGetObjectData(noteData); if (noodleData == null) { return; } Quaternion? cutQuaternion = noodleData.CutQuaternion; NoteJump noteJump = _noteJumpAccessor(ref ____noteMovement); NoteFloorMovement floorMovement = _noteFloorMovementAccessor(ref ____noteMovement); if (cutQuaternion.HasValue) { Quaternion quatVal = cutQuaternion.Value; _endRotationAccessor(ref noteJump) = quatVal; Vector3 vector = quatVal.eulerAngles; vector += _randomRotationsAccessor(ref noteJump)[_randomRotationIdxAccessor(ref noteJump)] * 20; Quaternion midrotation = Quaternion.Euler(vector); _middleRotationAccessor(ref noteJump) = midrotation; } Quaternion? worldRotationQuaternion = noodleData.WorldRotationQuaternion; Quaternion? localRotationQuaternion = noodleData.LocalRotationQuaternion; Transform transform = __instance.transform; Quaternion localRotation = Quaternion.identity; if (worldRotationQuaternion.HasValue || localRotationQuaternion.HasValue) { if (localRotationQuaternion.HasValue) { localRotation = localRotationQuaternion.Value; } if (worldRotationQuaternion.HasValue) { Quaternion quatVal = worldRotationQuaternion.Value; Quaternion inverseWorldRotation = Quaternion.Inverse(quatVal); _worldRotationJumpAccessor(ref noteJump) = quatVal; _inverseWorldRotationJumpAccessor(ref noteJump) = inverseWorldRotation; _worldRotationFloorAccessor(ref floorMovement) = quatVal; _inverseWorldRotationFloorAccessor(ref floorMovement) = inverseWorldRotation; quatVal *= localRotation; transform.localRotation = quatVal; } else { transform.localRotation *= localRotation; } } if (transform.localScale != Vector3.one) { transform.localScale = Vector3.one; // This is a fix for animation due to notes being recycled } IEnumerable? tracks = noodleData.Track; if (tracks != null) { foreach (Track track in tracks) { // add to gameobjects track.AddGameObject(__instance.gameObject); } } noodleData.EndRotation = endRotation; noodleData.MoveStartPos = moveStartPos; noodleData.MoveEndPos = moveEndPos; noodleData.JumpEndPos = jumpEndPos; noodleData.WorldRotation = __instance.worldRotation; noodleData.LocalRotation = localRotation; } private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Callvirt, _flipYSideGetter)) .InsertAndAdvance( new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _noteDataField)) .Advance(1) .Insert(new CodeInstruction(OpCodes.Call, _getFlipYSide)) .InstructionEnumeration(); } private static float GetFlipYSide(NoteData noteData, float @default) { float output = @default; NoodleNoteData? noodleData = TryGetObjectData(noteData); if (noodleData != null) { float? flipYSide = noodleData.FlipYSideInternal; if (flipYSide.HasValue) { output = flipYSide.Value; } } return output; } } [HeckPatch(typeof(NoteController))] [HeckPatch("ManualUpdate")] internal static class NoteControllerUpdate { internal static readonly FieldAccessor.Accessor _floorEndPosAccessor = FieldAccessor.GetAccessor("_endPos"); private static readonly FieldAccessor.Accessor _floorStartPosAccessor = FieldAccessor.GetAccessor("_startPos"); private static readonly FieldAccessor.Accessor _jumpStartPosAccessor = FieldAccessor.GetAccessor("_startPos"); private static readonly FieldAccessor.Accessor _jumpEndPosAccessor = FieldAccessor.GetAccessor("_endPos"); private static readonly FieldAccessor.Accessor _audioTimeSyncControllerAccessor = FieldAccessor.GetAccessor("_audioTimeSyncController"); private static readonly FieldAccessor.Accessor _jumpDurationAccessor = FieldAccessor.GetAccessor("_jumpDuration"); private static readonly FieldAccessor.Accessor _gameNoteBigCuttableAccessor = FieldAccessor.GetAccessor("_bigCuttableBySaberList"); private static readonly FieldAccessor.Accessor _gameNoteSmallCuttableAccessor = FieldAccessor.GetAccessor("_smallCuttableBySaberList"); private static readonly FieldAccessor.Accessor _bombNoteCuttableAccessor = FieldAccessor.GetAccessor("_cuttableBySaber"); internal static NoodleObjectData? NoodleData { get; private set; } private static void Prefix(NoteController __instance, NoteData ____noteData, NoteMovement ____noteMovement) { NoodleData = TryGetObjectData(____noteData); if (NoodleData == null) { return; } NoodleNoteData noodleData = (NoodleNoteData)NoodleData; IEnumerable? tracks = noodleData.Track; NoodleObjectData.AnimationObjectData? animationObject = noodleData.AnimationObject; if (tracks != null || animationObject != null) { NoteJump noteJump = NoteControllerInit._noteJumpAccessor(ref ____noteMovement); NoteFloorMovement floorMovement = NoteControllerInit._noteFloorMovementAccessor(ref ____noteMovement); // idk i just copied base game time float jumpDuration = _jumpDurationAccessor(ref noteJump); float elapsedTime = _audioTimeSyncControllerAccessor(ref noteJump).songTime - (____noteData.time - (jumpDuration * 0.5f)); elapsedTime = NoteJumpManualUpdate.NoteJumpTimeAdjust(elapsedTime, jumpDuration); float normalTime = elapsedTime / jumpDuration; Animation.AnimationHelper.GetObjectOffset(animationObject, tracks, normalTime, out Vector3? positionOffset, out Quaternion? rotationOffset, out Vector3? scaleOffset, out Quaternion? localRotationOffset, out float? dissolve, out float? dissolveArrow, out float? cuttable); if (positionOffset.HasValue) { Vector3 moveStartPos = noodleData.MoveStartPos; Vector3 moveEndPos = noodleData.MoveEndPos; Vector3 jumpEndPos = noodleData.JumpEndPos; Vector3 offset = positionOffset.Value; _floorStartPosAccessor(ref floorMovement) = moveStartPos + offset; _floorEndPosAccessor(ref floorMovement) = moveEndPos + offset; _jumpStartPosAccessor(ref noteJump) = moveEndPos + offset; _jumpEndPosAccessor(ref noteJump) = jumpEndPos + offset; } Transform transform = __instance.transform; if (rotationOffset.HasValue || localRotationOffset.HasValue) { Quaternion worldRotation = noodleData.WorldRotation; Quaternion localRotation = noodleData.LocalRotation; Quaternion worldRotationQuatnerion = worldRotation; if (rotationOffset.HasValue) { worldRotationQuatnerion *= rotationOffset.Value; Quaternion inverseWorldRotation = Quaternion.Inverse(worldRotationQuatnerion); NoteControllerInit._worldRotationJumpAccessor(ref noteJump) = worldRotationQuatnerion; NoteControllerInit._inverseWorldRotationJumpAccessor(ref noteJump) = inverseWorldRotation; NoteControllerInit._worldRotationFloorAccessor(ref floorMovement) = worldRotationQuatnerion; NoteControllerInit._inverseWorldRotationFloorAccessor(ref floorMovement) = inverseWorldRotation; } worldRotationQuatnerion *= localRotation; if (localRotationOffset.HasValue) { worldRotationQuatnerion *= localRotationOffset.Value; } transform.localRotation = worldRotationQuatnerion; } if (scaleOffset.HasValue) { transform.localScale = scaleOffset.Value; } if (dissolve.HasValue) { if (CutoutManager.NoteCutoutEffects.TryGetValue(__instance, out CutoutEffectWrapper cutoutEffect)) { cutoutEffect.SetCutout(dissolve.Value); } } if (dissolveArrow.HasValue && __instance.noteData.colorType != ColorType.None) { if (CutoutManager.NoteDisappearingArrowWrappers.TryGetValue(__instance, out DisappearingArrowWrapper disappearingArrowWrapper)) { disappearingArrowWrapper.SetCutout(dissolveArrow.Value); } } if (cuttable.HasValue) { bool enabled = cuttable.Value >= 1; switch (__instance) { case GameNoteController gameNoteController: BoxCuttableBySaber[] bigCuttableBySaberList = _gameNoteBigCuttableAccessor(ref gameNoteController); foreach (BoxCuttableBySaber bigCuttableBySaber in bigCuttableBySaberList) { if (bigCuttableBySaber.canBeCut != enabled) { bigCuttableBySaber.canBeCut = enabled; } } BoxCuttableBySaber[] smallCuttableBySaberList = _gameNoteSmallCuttableAccessor(ref gameNoteController); foreach (BoxCuttableBySaber smallCuttableBySaber in smallCuttableBySaberList) { if (smallCuttableBySaber.canBeCut != enabled) { smallCuttableBySaber.canBeCut = enabled; } } break; case BombNoteController bombNoteController: CuttableBySaber boxCuttableBySaber = _bombNoteCuttableAccessor(ref bombNoteController); if (boxCuttableBySaber.canBeCut != enabled) { boxCuttableBySaber.canBeCut = enabled; } break; } } } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/NoteFloorMovement.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; using NoodleExtensions.Animation; using UnityEngine; [HeckPatch(typeof(NoteFloorMovement))] [HeckPatch("ManualUpdate")] internal static class NoteFloorMovementManualUpdate { private static readonly FieldInfo _localPosition = AccessTools.Field(typeof(NoteFloorMovement), "_localPosition"); private static readonly MethodInfo _definiteNoteFloorMovement = AccessTools.Method(typeof(NoteFloorMovementManualUpdate), nameof(DefiniteNoteFloorMovement)); private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Stfld, _localPosition)) .Insert( new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Call, _definiteNoteFloorMovement)) .InstructionEnumeration(); } private static Vector3 DefiniteNoteFloorMovement(Vector3 original, NoteFloorMovement noteFloorMovement) { NoodleObjectData? noodleData = NoteControllerUpdate.NoodleData; if (noodleData != null) { AnimationHelper.GetDefinitePositionOffset(noodleData.AnimationObject, noodleData.Track, 0, out Vector3? position); if (position.HasValue) { Vector3 noteOffset = noodleData.NoteOffset; Vector3 endPos = NoteControllerUpdate._floorEndPosAccessor(ref noteFloorMovement); return original + (position.Value + noteOffset - endPos); } } return original; } } [HeckPatch(typeof(NoteFloorMovement))] [HeckPatch("SetToStart")] internal static class NoteFloorMovementSetToStart { private static void Postfix(Transform ____rotatedObject) { NoodleNoteData? noodleData = (NoodleNoteData?)NoteControllerUpdate.NoodleData; if (noodleData != null && noodleData.DisableLook) { ____rotatedObject.localRotation = Quaternion.Euler(0, 0, noodleData.EndRotation); } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/NoteJump.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; using IPA.Utilities; using NoodleExtensions.Animation; using UnityEngine; [HeckPatch(typeof(NoteJump))] [HeckPatch("ManualUpdate")] internal static class NoteJumpManualUpdate { private static readonly FieldInfo _threeQuartersMarkReportedField = AccessTools.Field(typeof(NoteJump), "_threeQuartersMarkReported"); private static readonly MethodInfo _localRotationSetter = AccessTools.PropertySetter(typeof(Transform), nameof(Transform.localRotation)); private static readonly FieldInfo _jumpDurationField = AccessTools.Field(typeof(NoteJump), "_jumpDuration"); private static readonly MethodInfo _noteJumpTimeAdjust = AccessTools.Method(typeof(NoteJumpManualUpdate), nameof(NoteJumpTimeAdjust)); private static readonly FieldInfo _localPositionField = AccessTools.Field(typeof(NoteJump), "_localPosition"); private static readonly MethodInfo _definiteNoteJump = AccessTools.Method(typeof(NoteJumpManualUpdate), nameof(DefiniteNoteJump)); private static readonly FieldInfo _definitePositionField = AccessTools.Field(typeof(NoteJumpManualUpdate), nameof(_definitePosition)); private static readonly MethodInfo _getTransform = AccessTools.PropertyGetter(typeof(Component), nameof(Component.transform)); private static readonly MethodInfo _doNoteLook = AccessTools.Method(typeof(NoteJumpManualUpdate), nameof(DoNoteLook)); private static readonly FieldInfo _startRotationField = AccessTools.Field(typeof(NoteJump), "_startRotation"); private static readonly FieldInfo _middleRotationField = AccessTools.Field(typeof(NoteJump), "_middleRotation"); private static readonly FieldInfo _endRotationField = AccessTools.Field(typeof(NoteJump), "_endRotation"); private static readonly FieldInfo _playerTransformsField = AccessTools.Field(typeof(NoteJump), "_playerTransforms"); private static readonly FieldInfo _rotatedObjectField = AccessTools.Field(typeof(NoteJump), "_rotatedObject"); private static readonly FieldInfo _inverseWorldRotationField = AccessTools.Field(typeof(NoteJump), "_inverseWorldRotation"); private static readonly FieldAccessor.Accessor _headTransformAccessor = FieldAccessor.GetAccessor("_headTransform"); // This field is used by reflection #pragma warning disable CS0414 private static bool _definitePosition = false; #pragma warning restore CS0414 internal static float NoteJumpTimeAdjust(float original, float jumpDuration) { NoodleObjectData? noodleData = NoteControllerUpdate.NoodleData; if (noodleData != null) { float? time = noodleData.Track?.Select(n => Heck.Animation.AnimationHelper.TryGetProperty(n, Plugin.TIME)).FirstOrDefault(n => n.HasValue); if (time.HasValue) { return time.Value * jumpDuration; } } return original; } private static IEnumerable Transpiler(IEnumerable instructions) { CodeMatcher codeMatcher = new CodeMatcher(instructions); object label; // CodeMatcher needs some better label manipulating methods // replace rotation stuff codeMatcher .MatchForward( false, new CodeMatch(OpCodes.Callvirt, _localRotationSetter)) .Advance(1); int endPos = codeMatcher.Pos; label = codeMatcher.Labels.First(); codeMatcher .MatchBack( false, new CodeMatch(null, label)) .Advance(1) .RemoveInstructions(endPos - codeMatcher.Pos) .InsertAndAdvance( new CodeInstruction(OpCodes.Ldloc_1), new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _startRotationField), new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _middleRotationField), new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _endRotationField), new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _playerTransformsField), new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _rotatedObjectField), new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Call, _getTransform), new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _inverseWorldRotationField), new CodeInstruction(OpCodes.Call, _doNoteLook)) // Add addition check to our quirky little variable to skip end position offset when we are using definitePosition .MatchForward( true, new CodeMatch(OpCodes.Ldfld, _threeQuartersMarkReportedField), new CodeMatch(OpCodes.Brfalse)); label = codeMatcher.Operand; codeMatcher .Advance(1) .Insert( new CodeInstruction(OpCodes.Ldsfld, _definitePositionField), new CodeInstruction(OpCodes.Brtrue_S, label)) .Start(); return codeMatcher // time adjust .MatchForward(false, new CodeMatch(OpCodes.Stloc_0)) .InsertAndAdvance( new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _jumpDurationField), new CodeInstruction(OpCodes.Call, _noteJumpTimeAdjust)) // final position .MatchForward(false, new CodeMatch(OpCodes.Stind_R4)) .Advance(2) .InsertAndAdvance( new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _localPositionField), new CodeInstruction(OpCodes.Ldloc_1), new CodeInstruction(OpCodes.Call, _definiteNoteJump), new CodeInstruction(OpCodes.Stfld, _localPositionField)) .InstructionEnumeration(); } private static Vector3 DefiniteNoteJump(Vector3 original, float time) { NoodleObjectData? noodleData = NoteControllerUpdate.NoodleData; if (noodleData != null) { AnimationHelper.GetDefinitePositionOffset(noodleData.AnimationObject, noodleData.Track, time, out Vector3? position); if (position.HasValue) { Vector3 noteOffset = noodleData.NoteOffset; _definitePosition = true; return position.Value + noteOffset; } } _definitePosition = false; return original; } // Performs all note look rotation from world space // Never want to touch this again.... private static void DoNoteLook( float num2, Quaternion startRotation, Quaternion middleRotation, Quaternion endRotation, PlayerTransforms playerTransforms, Transform rotatedObject, Transform baseTransform, Quaternion inverseWorldRotation) { NoodleNoteData? noodleData = (NoodleNoteData?)NoteControllerUpdate.NoodleData; if (noodleData != null && noodleData.DisableLook) { rotatedObject.localRotation = endRotation; return; } Quaternion a; if (num2 < 0.125f) { a = Quaternion.Slerp(baseTransform.rotation * startRotation, baseTransform.rotation * middleRotation, Mathf.Sin(num2 * Mathf.PI * 4f)); } else { a = Quaternion.Slerp(baseTransform.rotation * middleRotation, baseTransform.rotation * endRotation, Mathf.Sin((num2 - 0.125f) * Mathf.PI * 2f)); } Vector3 vector = playerTransforms.headWorldPos; // idk whats happening anymore Quaternion worldRot = inverseWorldRotation; if (baseTransform.parent != null) { // Handle parenting worldRot *= Quaternion.Inverse(baseTransform.parent.rotation); } // This line but super complicated so that "y" = "originTransform.up" // vector.y = Mathf.Lerp(vector.y, this._localPosition.y, 0.8f); Transform headTransform = _headTransformAccessor(ref playerTransforms); Quaternion inverse = Quaternion.Inverse(worldRot); Vector3 upVector = inverse * Vector3.up; float baseUpMagnitude = Vector3.Dot(worldRot * baseTransform.position, Vector3.up); float headUpMagnitude = Vector3.Dot(worldRot * headTransform.position, Vector3.up); float mult = Mathf.Lerp(headUpMagnitude, baseUpMagnitude, 0.8f) - headUpMagnitude; vector += upVector * mult; // more wtf Vector3 normalized = baseTransform.rotation * (worldRot * (baseTransform.position - vector).normalized); Quaternion b = Quaternion.LookRotation(normalized, rotatedObject.up); rotatedObject.rotation = Quaternion.Lerp(a, b, num2 * 2f); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/ObstacleController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; using Heck.Animation; using UnityEngine; using static NoodleExtensions.HarmonyPatches.SpawnDataHelper.BeatmapObjectSpawnMovementDataVariables; using static NoodleExtensions.NoodleObjectDataManager; using static NoodleExtensions.Plugin; [HeckPatch(typeof(ObstacleController))] [HeckPatch("Init")] internal static class ObstacleControllerInit { internal static readonly List _activeObstacles = new List(); private static readonly FieldInfo _worldRotationField = AccessTools.Field(typeof(ObstacleController), "_worldRotation"); private static readonly FieldInfo _inverseWorldRotationField = AccessTools.Field(typeof(ObstacleController), "_inverseWorldRotation"); private static readonly MethodInfo _widthGetter = AccessTools.PropertyGetter(typeof(ObstacleData), nameof(ObstacleData.width)); private static readonly FieldInfo _lengthField = AccessTools.Field(typeof(ObstacleController), "_length"); private static readonly MethodInfo _getCustomWidth = AccessTools.Method(typeof(ObstacleControllerInit), nameof(GetCustomWidth)); private static readonly MethodInfo _getWorldRotation = AccessTools.Method(typeof(ObstacleControllerInit), nameof(GetWorldRotation)); private static readonly MethodInfo _getCustomLength = AccessTools.Method(typeof(ObstacleControllerInit), nameof(GetCustomLength)); private static readonly MethodInfo _invertQuaternion = AccessTools.Method(typeof(ObstacleControllerInit), nameof(InvertQuaternion)); private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) // world rotation .MatchForward(false, new CodeMatch(OpCodes.Stfld, _worldRotationField)) .Insert( new CodeInstruction(OpCodes.Ldarg_1), new CodeInstruction(OpCodes.Ldarg_2), new CodeInstruction(OpCodes.Call, _getWorldRotation)) .RemoveInstructionsWithOffsets(-4, -1) // inverse world rotation .MatchForward(false, new CodeMatch(OpCodes.Stfld, _inverseWorldRotationField)) .Insert( new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldfld, _worldRotationField), new CodeInstruction(OpCodes.Call, _invertQuaternion)) .RemoveInstructionsWithOffsets(-5, -1) // width .MatchForward(false, new CodeMatch(OpCodes.Callvirt, _widthGetter)) .Advance(2) .InsertAndAdvance( new CodeInstruction(OpCodes.Ldarg_1), new CodeInstruction(OpCodes.Call, _getCustomWidth)) // length .MatchForward(false, new CodeMatch(OpCodes.Stfld, _lengthField)) .Insert( new CodeInstruction(OpCodes.Ldarg_1), new CodeInstruction(OpCodes.Call, _getCustomLength)) .InstructionEnumeration(); } private static void Postfix(ObstacleController __instance, Quaternion ____worldRotation, ObstacleData obstacleData, Vector3 ____startPos, Vector3 ____midPos, Vector3 ____endPos, ref Bounds ____bounds) { NoodleObstacleData? noodleData = TryGetObjectData(obstacleData); if (noodleData == null) { return; } Quaternion? localRotationQuaternion = noodleData.LocalRotationQuaternion; Transform transform = __instance.transform; Quaternion localRotation = Quaternion.identity; if (localRotationQuaternion.HasValue) { localRotation = localRotationQuaternion.Value; transform.localRotation = ____worldRotation * localRotation; } if (transform.localScale != Vector3.one) { transform.localScale = Vector3.one; // This is a fix for animation due to obstacles being recycled } IEnumerable? tracks = noodleData.Track; if (tracks != null) { foreach (Track track in tracks) { // add to gameobjects track.AddGameObject(__instance.gameObject); } } bool? cuttable = noodleData.Cuttable; if (cuttable.HasValue && !cuttable.Value) { ____bounds.size = Vector3.zero; } else { _activeObstacles.Add(__instance); } noodleData.StartPos = ____startPos; noodleData.MidPos = ____midPos; noodleData.EndPos = ____endPos; noodleData.LocalRotation = localRotation; noodleData.BoundsSize = ____bounds.size; } private static Quaternion InvertQuaternion(Quaternion quaternion) { return Quaternion.Inverse(quaternion); } private static Quaternion GetWorldRotation(ObstacleData obstacleData, float @default) { Quaternion worldRotation = Quaternion.Euler(0, @default, 0); NoodleObstacleData? noodleData = TryGetObjectData(obstacleData); if (noodleData != null) { Quaternion? worldRotationQuaternion = noodleData.WorldRotationQuaternion; if (worldRotationQuaternion.HasValue) { worldRotation = worldRotationQuaternion.Value; } noodleData.WorldRotation = worldRotation; } return worldRotation; } private static float GetCustomWidth(float @default, ObstacleData obstacleData) { NoodleObstacleData? noodleData = TryGetObjectData(obstacleData); if (noodleData != null) { float? width = noodleData.Width; if (width.HasValue) { return width.Value; } } return @default; } private static float GetCustomLength(float @default, ObstacleData obstacleData) { NoodleObstacleData? noodleData = TryGetObjectData(obstacleData); if (noodleData != null) { float? length = noodleData.Length; if (length.HasValue) { return length.Value * NoteLinesDistance; } } return @default; } } [HeckPatch(typeof(ObstacleController))] [HeckPatch("ManualUpdate")] internal static class ObstacleControllerManualUpdate { private static readonly FieldInfo _obstacleDataField = AccessTools.Field(typeof(ObstacleController), "_obstacleData"); private static readonly FieldInfo _move1DurationField = AccessTools.Field(typeof(ObstacleController), "_move1Duration"); private static readonly FieldInfo _finishMovementTime = AccessTools.Field(typeof(ObstacleController), "_finishMovementTime"); private static readonly MethodInfo _obstacleTimeAdjust = AccessTools.Method(typeof(ObstacleControllerManualUpdate), nameof(ObstacleTimeAdjust)); private static IEnumerable Transpiler(IEnumerable instructions) { List instructionList = instructions.ToList(); bool foundTime = false; for (int i = 0; i < instructionList.Count; i++) { if (!foundTime && instructionList[i].opcode == OpCodes.Stloc_0) { foundTime = true; instructionList.Insert(i, new CodeInstruction(OpCodes.Ldarg_0)); instructionList.Insert(i + 1, new CodeInstruction(OpCodes.Ldfld, _obstacleDataField)); instructionList.Insert(i + 2, new CodeInstruction(OpCodes.Ldarg_0)); instructionList.Insert(i + 3, new CodeInstruction(OpCodes.Ldfld, _move1DurationField)); instructionList.Insert(i + 4, new CodeInstruction(OpCodes.Ldarg_0)); instructionList.Insert(i + 5, new CodeInstruction(OpCodes.Ldfld, _finishMovementTime)); instructionList.Insert(i + 6, new CodeInstruction(OpCodes.Call, _obstacleTimeAdjust)); } } if (!foundTime) { Plugin.Logger.Log("Failed to find stloc.0!", IPA.Logging.Logger.Level.Error); } return instructionList.AsEnumerable(); } private static float ObstacleTimeAdjust(float original, ObstacleData obstacleData, float move1Duration, float finishMovementTime) { if (original > move1Duration) { NoodleObstacleData? noodleData = TryGetObjectData(obstacleData); if (noodleData != null) { float? time = noodleData.Track?.Select(n => AnimationHelper.TryGetProperty(n, TIME)).FirstOrDefault(n => n.HasValue); if (time.HasValue) { return (time.Value * (finishMovementTime - move1Duration)) + move1Duration; } } } return original; } private static void Prefix( ObstacleController __instance, ObstacleData ____obstacleData, AudioTimeSyncController ____audioTimeSyncController, float ____startTimeOffset, ref Vector3 ____startPos, ref Vector3 ____midPos, ref Vector3 ____endPos, float ____move1Duration, float ____move2Duration, float ____obstacleDuration, ref Quaternion ____worldRotation, ref Quaternion ____inverseWorldRotation, ref Bounds ____bounds) { NoodleObstacleData? noodleData = TryGetObjectData(____obstacleData); if (noodleData == null) { return; } IEnumerable? tracks = noodleData.Track; NoodleObjectData.AnimationObjectData? animationObject = noodleData.AnimationObject; if (tracks != null || animationObject != null) { // idk i just copied base game time float elapsedTime = ____audioTimeSyncController.songTime - ____startTimeOffset; float normalTime = (elapsedTime - ____move1Duration) / (____move2Duration + ____obstacleDuration); Animation.AnimationHelper.GetObjectOffset(animationObject, tracks, normalTime, out Vector3? positionOffset, out Quaternion? rotationOffset, out Vector3? scaleOffset, out Quaternion? localRotationOffset, out float? dissolve, out float? _, out float? cuttable); if (positionOffset.HasValue) { Vector3 startPos = noodleData.StartPos; Vector3 midPos = noodleData.MidPos; Vector3 endPos = noodleData.EndPos; Vector3 offset = positionOffset.Value; ____startPos = startPos + offset; ____midPos = midPos + offset; ____endPos = endPos + offset; } Transform transform = __instance.transform; if (rotationOffset.HasValue || localRotationOffset.HasValue) { Quaternion worldRotation = noodleData.WorldRotation; Quaternion localRotation = noodleData.LocalRotation; Quaternion worldRotationQuatnerion = worldRotation; if (rotationOffset.HasValue) { worldRotationQuatnerion *= rotationOffset.Value; Quaternion inverseWorldRotation = Quaternion.Inverse(worldRotationQuatnerion); ____worldRotation = worldRotationQuatnerion; ____inverseWorldRotation = inverseWorldRotation; } worldRotationQuatnerion *= localRotation; if (localRotationOffset.HasValue) { worldRotationQuatnerion *= localRotationOffset.Value; } transform.localRotation = worldRotationQuatnerion; } if (cuttable.HasValue) { if (cuttable.Value >= 1) { if (____bounds.size != Vector3.zero) { ____bounds.size = Vector3.zero; } } else { Vector3 boundsSize = noodleData.BoundsSize; if (____bounds.size != boundsSize) { ____bounds.size = boundsSize; } } } if (scaleOffset.HasValue) { transform.localScale = scaleOffset.Value; } if (dissolve.HasValue) { if (CutoutManager.ObstacleCutoutEffects.TryGetValue(__instance, out CutoutAnimateEffectWrapper cutoutAnimateEffect)) { cutoutAnimateEffect.SetCutout(dissolve.Value); } } } if (noodleData.DoUnhide) { __instance.hide = false; } } } [HeckPatch(typeof(ObstacleController))] [HeckPatch("GetPosForTime")] internal static class ObstacleControllerGetPosForTime { private static bool Prefix( ref Vector3 __result, ObstacleData ____obstacleData, Vector3 ____startPos, Vector3 ____midPos, float ____move1Duration, float ____move2Duration, float ____obstacleDuration, float time) { NoodleObstacleData? noodleData = TryGetObjectData(____obstacleData); if (noodleData == null) { return true; } float jumpTime = Mathf.Clamp((time - ____move1Duration) / (____move2Duration + ____obstacleDuration), 0, 1); Animation.AnimationHelper.GetDefinitePositionOffset(noodleData.AnimationObject, noodleData.Track, jumpTime, out Vector3? position); if (position.HasValue) { Vector3 noteOffset = noodleData.NoteOffset; Vector3 definitePosition = position.Value + noteOffset; definitePosition.x += noodleData.XOffset; if (time < ____move1Duration) { __result = Vector3.LerpUnclamped(____startPos, ____midPos, time / ____move1Duration); __result += definitePosition - ____midPos; } else { __result = definitePosition; } return false; } return true; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SceneTransition/MissionLevelScenesTransitionSetupDataSO.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using HarmonyLib; [HarmonyPatch( typeof(MissionLevelScenesTransitionSetupDataSO), new Type[] { typeof(string), typeof(IDifficultyBeatmap), typeof(IPreviewBeatmapLevel), typeof(MissionObjective[]), typeof(ColorScheme), typeof(GameplayModifiers), typeof(PlayerSpecificSettings), typeof(string) })] [HarmonyPatch("Init")] internal static class MissionLevelScenesTransitionSetupDataSOInit { private static void Postfix(IDifficultyBeatmap difficultyBeatmap) { SceneTransitionHelper.Patch(difficultyBeatmap); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SceneTransition/MultiplayerLevelScenesTransitionSetupDataSO.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using HarmonyLib; [HarmonyPatch( typeof(MultiplayerLevelScenesTransitionSetupDataSO), new Type[] { typeof(string), typeof(IPreviewBeatmapLevel), typeof(BeatmapDifficulty), typeof(BeatmapCharacteristicSO), typeof(IDifficultyBeatmap), typeof(ColorScheme), typeof(GameplayModifiers), typeof(PlayerSpecificSettings), typeof(PracticeSettings), typeof(bool) })] [HarmonyPatch("Init")] internal static class MultiplayerLevelScenesTransitionSetupDataSOInit { private static void Postfix(IDifficultyBeatmap difficultyBeatmap) { SceneTransitionHelper.Patch(difficultyBeatmap); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SceneTransition/SceneTransitionHelper.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Linq; using CustomJSONData; using CustomJSONData.CustomBeatmap; using static NoodleExtensions.Plugin; internal static class SceneTransitionHelper { internal static void Patch(IDifficultyBeatmap difficultyBeatmap) { if (difficultyBeatmap.beatmapData is CustomBeatmapData customBeatmapData) { IEnumerable? requirements = customBeatmapData.beatmapCustomData.Get>("_requirements")?.Cast(); bool noodleRequirement = requirements?.Contains(CAPABILITY) ?? false; NoodleController.ToggleNoodlePatches(noodleRequirement); } } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SceneTransition/StandardLevelScenesTransitionSetupDataSO.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using HarmonyLib; [HarmonyPatch( typeof(StandardLevelScenesTransitionSetupDataSO), new Type[] { typeof(string), typeof(IDifficultyBeatmap), typeof(IPreviewBeatmapLevel), typeof(OverrideEnvironmentSettings), typeof(ColorScheme), typeof(GameplayModifiers), typeof(PlayerSpecificSettings), typeof(PracticeSettings), typeof(string), typeof(bool) })] [HarmonyPatch("Init")] internal static class StandardLevelScenesTransitionSetupDataSOInit { private static void Postfix(IDifficultyBeatmap difficultyBeatmap) { SceneTransitionHelper.Patch(difficultyBeatmap); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SceneTransition/TutorialScenesTransitionSetupDataSO.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using HarmonyLib; // Force disable Noodle Extensions in tutorial scene [HarmonyPatch(typeof(TutorialScenesTransitionSetupDataSO))] [HarmonyPatch("Init")] internal static class TutorialScenesTransitionSetupDataSOInit { private static void Prefix() { NoodleController.ToggleNoodlePatches(false); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SmallFixes/BasicBeatmapObjectManager.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using System.Collections.Generic; using Heck; // Do not add fake obstacles to active obstacles to increase performance [HeckPatch(typeof(BasicBeatmapObjectManager))] [HeckPatch("Init")] internal static class BasicBeatmapObjectManagerInit { private static void Postfix() { ObstacleControllerInit._activeObstacles.Clear(); } } [HeckPatch(typeof(BasicBeatmapObjectManager))] [HeckPatch("get_activeObstacleControllers")] internal static class BasicBeatmapObjectManagerGetActiveObstacleControllers { private static bool Prefix(ref List __result) { __result = ObstacleControllerInit._activeObstacles; return false; } } [HeckPatch(typeof(BasicBeatmapObjectManager))] [HeckPatch("DespawnInternal")] [HeckPatch(new Type[] { typeof(ObstacleController) })] internal static class BasicBeatmapObjectManagerDespawnInternal { private static void Postfix(ObstacleController obstacleController) { ObstacleControllerInit._activeObstacles.Remove(obstacleController); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SmallFixes/BeatEffectSpawner.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; using UnityEngine; // Readjust spawn effect to take global position instead of local [HeckPatch(typeof(BeatEffectSpawner))] [HeckPatch("HandleNoteDidStartJump")] internal static class BeatEffectSpawnerHandleNoteDidStartJump { private static readonly MethodInfo _jumpStartPosGetter = AccessTools.PropertyGetter(typeof(NoteController), nameof(NoteController.jumpStartPos)); private static readonly MethodInfo _beatEffectInit = AccessTools.Method(typeof(BeatEffect), nameof(BeatEffect.Init)); private static readonly MethodInfo _getNoteControllerPosition = AccessTools.Method(typeof(BeatEffectSpawnerHandleNoteDidStartJump), nameof(GetNoteControllerPosition)); private static readonly MethodInfo _getNoteControllerRotation = AccessTools.Method(typeof(BeatEffectSpawnerHandleNoteDidStartJump), nameof(GetNoteControllerRotation)); private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) // position .MatchForward(false, new CodeMatch(OpCodes.Callvirt, _jumpStartPosGetter)) .Advance(-2) .InsertAndAdvance(new CodeInstruction(OpCodes.Call, _getNoteControllerPosition)) .RemoveInstructions(4) // rotation .MatchForward(false, new CodeMatch(OpCodes.Callvirt, _beatEffectInit)) .Advance(-1) .Set(OpCodes.Call, _getNoteControllerRotation) .InstructionEnumeration(); } private static Vector3 GetNoteControllerPosition(NoteController noteController) { return noteController.transform.position; } private static Quaternion GetNoteControllerRotation(NoteController noteController) { return noteController.transform.rotation; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SmallFixes/BeatmapObjectManager.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; using static NoodleExtensions.NoodleObjectDataManager; // TODO: find out what actually causes obstacle flickering [HeckPatch(typeof(BeatmapObjectManager))] [HeckPatch("SpawnObstacle")] internal static class BeatmapObjectManagerSpawnObstacle { private static readonly MethodInfo _spawnhiddenGetter = AccessTools.PropertyGetter(typeof(BeatmapObjectManager), nameof(BeatmapObjectManager.spawnHidden)); private static readonly MethodInfo _getHiddenForType = AccessTools.Method(typeof(BeatmapObjectManagerSpawnObstacle), nameof(GetHiddenForType)); private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Call, _spawnhiddenGetter)) .SetOperandAndAdvance(_getHiddenForType) .InstructionEnumeration(); } private static void Postfix(ObstacleController __result) { NoodleObstacleData? noodleData = TryGetObjectData(__result.obstacleData); if (noodleData != null) { noodleData.DoUnhide = true; } } private static bool GetHiddenForType(BeatmapObjectManager beatmapObjectManager) { if (beatmapObjectManager is BasicBeatmapObjectManager) { return true; } return beatmapObjectManager.spawnHidden; } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SmallFixes/CutoutEffect.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System; using Heck; using UnityEngine; // Do not run SetCutout if the new value is the same as old. [HeckPatch(typeof(CutoutEffect))] [HeckPatch("SetCutout")] [HeckPatch(new Type[] { typeof(float), typeof(Vector3) })] internal static class CutoutEffectSetCutout { private static bool Prefix(float cutout, float ____cutout) { return !(cutout == ____cutout); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SmallFixes/DisappearingArrowController.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using Heck; [HeckPatch(typeof(DisappearingArrowControllerBase))] [HeckPatch("SetArrowTransparency")] internal static class DisappearingArrowControllerSetArrowTransparency { // This makes _dissolveArrow work and I cannot figure out why private static void Postfix(CutoutEffect ____arrowCutoutEffect, float arrowTransparency) { ____arrowCutoutEffect.SetCutout(1f - arrowTransparency); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SmallFixes/MemoryPoolBase.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using Heck; using ModestTree; using Zenject; [HeckPatch(typeof(MemoryPoolBase))] [HeckPatch("Despawn")] internal static class MemoryPoolBaseObstacleControllerDespawn { private static readonly MethodInfo _assertThat = AccessTools.Method(typeof(Assert), nameof(Assert.That), new System.Type[] { typeof(bool), typeof(string), typeof(object) }); // This stupid assert runs a Contains() which is laggy when spawning an insane amount of walls private static IEnumerable Transpiler(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, new CodeMatch(OpCodes.Call, _assertThat)) .RemoveInstructionsWithOffsets(-9, 0) .InstructionEnumeration(); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SmallFixes/PlayerTransforms.cs ================================================ namespace NoodleExtensions.HarmonyPatches { /*using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using HarmonyLib;*/ using Heck; using UnityEngine; // Possibly breaks in multiplayer? We don't support that anyways. // edit: we do support it, but it hasnt broken yet so fuck it [HeckPatch(typeof(PlayerTransforms))] [HeckPatch("Update")] internal static class PlayerTransformsUpdate { private static void Postfix(ref Vector3 ____headPseudoLocalPos, Transform ____headTransform) { ____headPseudoLocalPos = ____headTransform.localPosition; } } /*[NoodlePatch(typeof(PlayerTransforms))] [NoodlePatch("MoveTowardsHead")] internal static class PlayerTransformsMoveTowardsHead { private static readonly MethodInfo _headOffsetZNotPsuedo = SymbolExtensions.GetMethodInfo(() => HeadOffsetZNotPsuedo(Quaternion.identity, Vector3.zero)); private static readonly FieldInfo _headTransformField = AccessTools.Field(typeof(PlayerTransforms), "_headTransform"); private static readonly MethodInfo _localPositionMethod = typeof(Transform).GetProperty("localPosition").GetGetMethod(); private static IEnumerable Transpiler(IEnumerable instructions) { List instructionList = instructions.ToList(); bool foundHeadLocalPos = false; for (int i = 0; i < instructionList.Count; i++) { if (!foundHeadLocalPos && instructionList[i].opcode == OpCodes.Call && ((MethodInfo)instructionList[i].operand).Name == "HeadOffsetZ") { foundHeadLocalPos = true; instructionList[i].operand = _headOffsetZNotPsuedo; instructionList.Insert(i, new CodeInstruction(OpCodes.Ldarg_0)); instructionList.Insert(i + 1, new CodeInstruction(OpCodes.Ldfld, _headTransformField)); instructionList.Insert(i + 2, new CodeInstruction(OpCodes.Callvirt, _localPositionMethod)); instructionList.RemoveAt(0); } } if (!foundHeadLocalPos) { NoodleExtensions.Plugin.Logger.Log("Failed to find call to HeadOffsetZ!", IPA.Logging.Logger.Level.Error); } return instructionList.AsEnumerable(); } private static float HeadOffsetZNotPsuedo(Quaternion noteInverseWorldRotation, Vector3 localPos) { return (noteInverseWorldRotation * localPos).z; } }*/ } ================================================ FILE: NoodleExtensions/HarmonyPatches/SpawnDataHelper.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using IPA.Utilities; using UnityEngine; using static NoodleExtensions.HarmonyPatches.SpawnDataHelper.BeatmapObjectSpawnMovementDataVariables; internal static class SpawnDataHelper { internal static Vector3 GetNoteOffset(BeatmapObjectData beatmapObjectData, float? startRow, float? startHeight) { float distance = (-(NoteLinesCount - 1f) * 0.5f) + (startRow.HasValue ? NoteLinesCount / 2f : 0); // Add last part to simulate https://github.com/spookyGh0st/beatwalls/#wall float lineIndex = startRow.GetValueOrDefault(beatmapObjectData.lineIndex); distance = (distance + lineIndex) * NoteLinesDistance; return (RightVec * distance) + new Vector3(0, LineYPosForLineLayer(beatmapObjectData, startHeight), 0); } internal static float LineYPosForLineLayer(BeatmapObjectData beatmapObjectData, float? height) { float ypos = BaseLinesYPos; if (height.HasValue) { ypos = (height.Value * NoteLinesDistance) + BaseLinesYPos; // offset by 0.25 } else if (beatmapObjectData is NoteData noteData) { ypos = BeatmapObjectSpawnMovementData.LineYPosForLineLayer(noteData.noteLineLayer); } return ypos; } internal static void GetNoteJumpValues( float? inputNoteJumpMovementSpeed, float? inputNoteJumpStartBeatOffset, out float localJumpDuration, out float localJumpDistance, out Vector3 localMoveStartPos, out Vector3 localMoveEndPos, out Vector3 localJumpEndPos) { float localNoteJumpMovementSpeed = inputNoteJumpMovementSpeed ?? NoteJumpMovementSpeed; float localNoteJumpStartBeatOffset = inputNoteJumpStartBeatOffset ?? NoteJumpStartBeatOffset; float num = 60f / StartBPM; float num2 = StartHalfJumpDurationInBeats; while (localNoteJumpMovementSpeed * num * num2 > MaxHalfJumpDistance) { num2 /= 2f; } num2 += localNoteJumpStartBeatOffset; if (num2 < 1f) { num2 = 1f; } localJumpDuration = num * num2 * 2f; localJumpDistance = localNoteJumpMovementSpeed * localJumpDuration; localMoveStartPos = CenterPos + (ForwardVec * (MoveDistance + (localJumpDistance * 0.5f))); localMoveEndPos = CenterPos + (ForwardVec * localJumpDistance * 0.5f); localJumpEndPos = CenterPos - (ForwardVec * localJumpDistance * 0.5f); } internal static void InitBeatmapObjectSpawnController(BeatmapObjectSpawnMovementData beatmapObjectSpawnMovementData) { BeatmapObjectSpawnMovementData = beatmapObjectSpawnMovementData; } internal static class BeatmapObjectSpawnMovementDataVariables { private static readonly FieldAccessor.Accessor _startBPMAccessor = FieldAccessor.GetAccessor("_startBpm"); private static readonly FieldAccessor.Accessor _topObstaclePosYAccessor = FieldAccessor.GetAccessor("_topObstaclePosY"); private static readonly FieldAccessor.Accessor _jumpOffsetYAccessor = FieldAccessor.GetAccessor("_jumpOffsetY"); private static readonly FieldAccessor.Accessor _verticalObstaclePosYAccessor = FieldAccessor.GetAccessor("_verticalObstaclePosY"); private static readonly FieldAccessor.Accessor _moveDistanceAccessor = FieldAccessor.GetAccessor("_moveDistance"); private static readonly FieldAccessor.Accessor _noteJumpMovementSpeedAccessor = FieldAccessor.GetAccessor("_noteJumpMovementSpeed"); private static readonly FieldAccessor.Accessor _noteJumpStartBeatOffsetAccessor = FieldAccessor.GetAccessor("_noteJumpStartBeatOffset"); private static readonly FieldAccessor.Accessor _noteLinesDistanceAccessor = FieldAccessor.GetAccessor("_noteLinesDistance"); private static readonly FieldAccessor.Accessor _baseLinesYPosAccessor = FieldAccessor.GetAccessor("_baseLinesYPos"); private static readonly FieldAccessor.Accessor _noteLinesCountAccessor = FieldAccessor.GetAccessor("_noteLinesCount"); private static readonly FieldAccessor.Accessor _centerPosAccessor = FieldAccessor.GetAccessor("_centerPos"); private static readonly FieldAccessor.Accessor _forwardVecAccessor = FieldAccessor.GetAccessor("_forwardVec"); private static readonly FieldAccessor.Accessor _rightVecAccessor = FieldAccessor.GetAccessor("_rightVec"); private static readonly FieldAccessor.Accessor _startHalfJumpDurationInBeatsAccessor = FieldAccessor.GetAccessor("_startHalfJumpDurationInBeats"); private static readonly FieldAccessor.Accessor _maxHalfJumpDistanceAccessor = FieldAccessor.GetAccessor("_maxHalfJumpDistance"); private static BeatmapObjectSpawnMovementData? _beatmapObjectSpawnMovementData; internal static BeatmapObjectSpawnMovementData BeatmapObjectSpawnMovementData { get => _beatmapObjectSpawnMovementData!; set => _beatmapObjectSpawnMovementData = value; } internal static float StartBPM => _startBPMAccessor(ref _beatmapObjectSpawnMovementData!); internal static float TopObstaclePosY => _topObstaclePosYAccessor(ref _beatmapObjectSpawnMovementData!); internal static float JumpOffsetY => _jumpOffsetYAccessor(ref _beatmapObjectSpawnMovementData!); internal static float VerticalObstaclePosY => _verticalObstaclePosYAccessor(ref _beatmapObjectSpawnMovementData!); internal static float MoveDistance => _moveDistanceAccessor(ref _beatmapObjectSpawnMovementData!); internal static float NoteJumpMovementSpeed => _noteJumpMovementSpeedAccessor(ref _beatmapObjectSpawnMovementData!); internal static float NoteJumpStartBeatOffset => _noteJumpStartBeatOffsetAccessor(ref _beatmapObjectSpawnMovementData!); internal static float NoteLinesDistance => _noteLinesDistanceAccessor(ref _beatmapObjectSpawnMovementData!); internal static float BaseLinesYPos => _baseLinesYPosAccessor(ref _beatmapObjectSpawnMovementData!); internal static float NoteLinesCount => _noteLinesCountAccessor(ref _beatmapObjectSpawnMovementData!); internal static Vector3 CenterPos => _centerPosAccessor(ref _beatmapObjectSpawnMovementData!); internal static Vector3 ForwardVec => _forwardVecAccessor(ref _beatmapObjectSpawnMovementData!); internal static Vector3 RightVec => _rightVecAccessor(ref _beatmapObjectSpawnMovementData!); internal static float StartHalfJumpDurationInBeats => _startHalfJumpDurationInBeatsAccessor(ref _beatmapObjectSpawnMovementData!); internal static float MaxHalfJumpDistance => _maxHalfJumpDistanceAccessor(ref _beatmapObjectSpawnMovementData!); } } } ================================================ FILE: NoodleExtensions/HarmonyPatches/SpawnRotationProcessor.cs ================================================ namespace NoodleExtensions.HarmonyPatches { using CustomJSONData; using CustomJSONData.CustomBeatmap; using Heck; using static NoodleExtensions.Plugin; // i still dont even know if this works, but must maintain feature parity with mapping extensions! [HeckPatch(typeof(SpawnRotationProcessor))] [HeckPatch("ProcessBeatmapEventData")] internal static class SpawnRotationProcessorProcessBeatmapEventData { private static bool Prefix(BeatmapEventData beatmapEventData, ref float ____rotation) { if (beatmapEventData.type.IsRotationEvent() && beatmapEventData is CustomBeatmapEventData customData) { float? rotation = customData.customData.Get(ROTATION); if (rotation.HasValue) { ____rotation += rotation.Value; return false; } } return true; } } } ================================================ FILE: NoodleExtensions/NoodleController.cs ================================================ namespace NoodleExtensions { using static NoodleExtensions.Plugin; public static class NoodleController { public static bool NoodleExtensionsActive { get; private set; } public static void ToggleNoodlePatches(bool value) { if (value != NoodleExtensionsActive) { Heck.HeckData.TogglePatches(_harmonyInstance, value); NoodleExtensionsActive = value; if (NoodleExtensionsActive) { CustomJSONData.CustomEventCallbackController.didInitEvent += Animation.AnimationController.CustomEventCallbackInit; } else { CustomJSONData.CustomEventCallbackController.didInitEvent -= Animation.AnimationController.CustomEventCallbackInit; } } } } } ================================================ FILE: NoodleExtensions/NoodleExtensions.csproj ================================================  net48 Library 9 enable false ..\Refs $(LocalRefsDir) $(MSBuildProjectDirectory)\ true portable True True True $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False $(BeatSaberDir)\Libs\0Harmony.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll False $(BeatSaberDir)\Plugins\Heck.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll False $(BeatSaberDir)\Plugins\CustomJSONData.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Colors.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll False $(BeatSaberDir)\Plugins\SongCore.dll False all runtime; build; native; contentfiles; analyzers; buildtransitive 1.2.0-beta.354 runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: NoodleExtensions/NoodleExtensionsExtensions.cs ================================================ namespace NoodleExtensions { using System; using System.Collections.Generic; using CustomJSONData.CustomBeatmap; internal static class NoodleExtensionsExtensions { internal static Dictionary GetDataForObject(this BeatmapObjectData beatmapObjectData) { Dictionary dynData; switch (beatmapObjectData) { case CustomObstacleData data: dynData = data.customData; break; case CustomNoteData data: dynData = data.customData; break; case CustomWaypointData data: dynData = data.customData; break; default: throw new InvalidOperationException($"beatmapObjectdata was not of type CustomObstacleData, CustomNoteData, or CustomWaypointData. Was: {beatmapObjectData.GetType().FullName}"); } return dynData; } } } ================================================ FILE: NoodleExtensions/NoodleObjectData.cs ================================================ namespace NoodleExtensions { using System; using System.Collections.Generic; using System.Linq; using CustomJSONData; using CustomJSONData.CustomBeatmap; using Heck; using Heck.Animation; using UnityEngine; using static NoodleExtensions.Plugin; internal static class NoodleObjectDataManager { private static Dictionary _noodleObjectDatas = new Dictionary(); internal static T? TryGetObjectData(BeatmapObjectData beatmapObjectData) { if (_noodleObjectDatas.TryGetValue(beatmapObjectData, out NoodleObjectData noodleObjectData)) { if (noodleObjectData is T t) { return t; } else { throw new InvalidOperationException($"NoodleObjectData was not of correct type. Expected: {typeof(T).Name}, was: {noodleObjectData.GetType().Name}"); } } return default; } internal static void DeserializeBeatmapData(IReadonlyBeatmapData beatmapData) { _noodleObjectDatas = new Dictionary(); foreach (BeatmapObjectData beatmapObjectData in beatmapData.beatmapObjectsData) { try { NoodleObjectData noodleObjectData; Dictionary customData; switch (beatmapObjectData) { case CustomObstacleData customObstacleData: customData = customObstacleData.customData; noodleObjectData = ProcessCustomObstacle(customData); break; case CustomNoteData customNoteData: customData = customNoteData.customData; noodleObjectData = ProcessCustomNote(customData); break; case CustomWaypointData customWaypointData: customData = customWaypointData.customData; noodleObjectData = new NoodleObjectData(); break; default: continue; } if (noodleObjectData != null) { FinalizeCustomObject(customData, noodleObjectData, beatmapData); _noodleObjectDatas.Add(beatmapObjectData, noodleObjectData); } } catch (Exception e) { Plugin.Logger.Log($"Could not create NoodleObjectData for object {beatmapObjectData.GetType().Name} at {beatmapObjectData.time}", IPA.Logging.Logger.Level.Error); Plugin.Logger.Log(e, IPA.Logging.Logger.Level.Error); } } } private static void FinalizeCustomObject(Dictionary dynData, NoodleObjectData noodleObjectData, IReadonlyBeatmapData beatmapData) { object? rotation = dynData.Get(ROTATION); if (rotation != null) { if (rotation is List list) { IEnumerable rot = list.Select(n => Convert.ToSingle(n)); noodleObjectData.WorldRotationQuaternion = Quaternion.Euler(rot.ElementAt(0), rot.ElementAt(1), rot.ElementAt(2)); } else { noodleObjectData.WorldRotationQuaternion = Quaternion.Euler(0, Convert.ToSingle(rotation), 0); } } IEnumerable? localrot = dynData.Get>(LOCALROTATION)?.Select(n => Convert.ToSingle(n)); if (localrot != null) { noodleObjectData.LocalRotationQuaternion = Quaternion.Euler(localrot.ElementAt(0), localrot.ElementAt(1), localrot.ElementAt(2)); } noodleObjectData.Track = AnimationHelper.GetTrackArray(dynData, beatmapData); Dictionary? animationObjectDyn = dynData.Get>("_animation"); if (animationObjectDyn != null) { Dictionary? pointDefinitions = ((CustomBeatmapData)beatmapData).customData.Get>("pointDefinitions") ?? throw new InvalidOperationException("Could not retrieve point definitions."); Animation.AnimationHelper.GetAllPointData( animationObjectDyn, pointDefinitions, out PointDefinition? localPosition, out PointDefinition? localRotation, out PointDefinition? localScale, out PointDefinition? localLocalRotation, out PointDefinition? localDissolve, out PointDefinition? localDissolveArrow, out PointDefinition? localCuttable, out PointDefinition? localDefinitePosition); NoodleObjectData.AnimationObjectData animationObjectData = new NoodleObjectData.AnimationObjectData { LocalPosition = localPosition, LocalRotation = localRotation, LocalScale = localScale, LocalLocalRotation = localLocalRotation, LocalDissolve = localDissolve, LocalDissolveArrow = localDissolveArrow, LocalCuttable = localCuttable, LocalDefinitePosition = localDefinitePosition, }; noodleObjectData.AnimationObject = animationObjectData; } noodleObjectData.Cuttable = dynData.Get(CUTTABLE); noodleObjectData.Fake = dynData.Get(FAKENOTE); IEnumerable? position = dynData.GetNullableFloats(POSITION); noodleObjectData.StartX = position?.ElementAtOrDefault(0); noodleObjectData.StartY = position?.ElementAtOrDefault(1); noodleObjectData.NJS = dynData.Get(NOTEJUMPSPEED); noodleObjectData.SpawnOffset = dynData.Get(NOTESPAWNOFFSET); noodleObjectData.AheadTimeInternal = dynData.Get("aheadTime"); } private static NoodleNoteData ProcessCustomNote(Dictionary dynData) { NoodleNoteData noodleNoteData = new NoodleNoteData(); float? cutDir = dynData.Get(CUTDIRECTION); if (cutDir.HasValue) { noodleNoteData.CutQuaternion = Quaternion.Euler(0, 0, cutDir.Value); } noodleNoteData.FlipYSideInternal = dynData.Get("flipYSide"); noodleNoteData.FlipLineIndexInternal = dynData.Get("flipLineIndex"); noodleNoteData.StartNoteLineLayerInternal = dynData.Get("startNoteLineLayer"); noodleNoteData.DisableGravity = dynData.Get(NOTEGRAVITYDISABLE) ?? false; noodleNoteData.DisableLook = dynData.Get(NOTELOOKDISABLE) ?? false; return noodleNoteData; } private static NoodleObstacleData ProcessCustomObstacle(Dictionary dynData) { NoodleObstacleData noodleObstacleData = new NoodleObstacleData(); IEnumerable? scale = dynData.GetNullableFloats(SCALE); noodleObstacleData.Width = scale?.ElementAtOrDefault(0); noodleObstacleData.Height = scale?.ElementAtOrDefault(1); noodleObstacleData.Length = scale?.ElementAtOrDefault(2); return noodleObstacleData; } } internal record NoodleNoteData : NoodleObjectData { internal Quaternion? CutQuaternion { get; set; } internal Vector3 MoveStartPos { get; set; } internal Vector3 MoveEndPos { get; set; } internal Vector3 JumpEndPos { get; set; } internal float? FlipYSideInternal { get; set; } internal float? FlipLineIndexInternal { get; set; } internal float? StartNoteLineLayerInternal { get; set; } internal bool DisableGravity { get; set; } internal bool DisableLook { get; set; } internal float EndRotation { get; set; } } internal record NoodleObstacleData : NoodleObjectData { internal Vector3 StartPos { get; set; } internal Vector3 MidPos { get; set; } internal Vector3 EndPos { get; set; } internal Vector3 BoundsSize { get; set; } internal float? Width { get; set; } internal float? Height { get; set; } internal float? Length { get; set; } internal float XOffset { get; set; } internal bool DoUnhide { get; set; } } internal record NoodleObjectData { internal Quaternion? WorldRotationQuaternion { get; set; } internal Quaternion? LocalRotationQuaternion { get; set; } internal IEnumerable? Track { get; set; } internal Quaternion WorldRotation { get; set; } internal Quaternion LocalRotation { get; set; } internal AnimationObjectData? AnimationObject { get; set; } internal Vector3 NoteOffset { get; set; } internal bool? Cuttable { get; set; } internal bool? Fake { get; set; } internal float? StartX { get; set; } internal float? StartY { get; set; } internal float? NJS { get; set; } internal float? SpawnOffset { get; set; } internal float? AheadTimeInternal { get; set; } internal record AnimationObjectData { internal PointDefinition? LocalPosition { get; set; } internal PointDefinition? LocalRotation { get; set; } internal PointDefinition? LocalScale { get; set; } internal PointDefinition? LocalLocalRotation { get; set; } internal PointDefinition? LocalDissolve { get; set; } internal PointDefinition? LocalDissolveArrow { get; set; } internal PointDefinition? LocalCuttable { get; set; } internal PointDefinition? LocalDefinitePosition { get; set; } } } } ================================================ FILE: NoodleExtensions/Plugin.cs ================================================ namespace NoodleExtensions { using System.Reflection; using HarmonyLib; using Heck; using IPA; using IPALogger = IPA.Logging.Logger; [Plugin(RuntimeOptions.DynamicInit)] internal class Plugin { internal const string CAPABILITY = "Noodle Extensions"; internal const string HARMONYIDCORE = "com.aeroluna.BeatSaber.NoodleExtensionsCore"; internal const string HARMONYID = "com.aeroluna.BeatSaber.NoodleExtensions"; internal const string CUTDIRECTION = "_cutDirection"; internal const string CUTTABLE = "_interactable"; internal const string DEFINITEPOSITION = "_definitePosition"; internal const string DISSOLVE = "_dissolve"; internal const string DISSOLVEARROW = "_dissolveArrow"; internal const string FAKENOTE = "_fake"; internal const string FLIP = "_flip"; internal const string LOCALROTATION = "_localRotation"; internal const string NOTEGRAVITYDISABLE = "_disableNoteGravity"; internal const string NOTEJUMPSPEED = "_noteJumpMovementSpeed"; internal const string NOTELOOKDISABLE = "_disableNoteLook"; internal const string NOTESPAWNOFFSET = "_noteJumpStartBeatOffset"; internal const string POSITION = "_position"; internal const string ROTATION = "_rotation"; internal const string SCALE = "_scale"; internal const string TIME = "_time"; internal const string TRACK = "_track"; internal const string ASSIGNPLAYERTOTRACK = "AssignPlayerToTrack"; internal const string ASSIGNTRACKPARENT = "AssignTrackParent"; internal static readonly Harmony _harmonyInstanceCore = new Harmony(HARMONYIDCORE); internal static readonly Harmony _harmonyInstance = new Harmony(HARMONYID); #pragma warning disable CS8618 internal static HeckLogger Logger { get; private set; } #pragma warning restore CS8618 [Init] public void Init(IPALogger pluginLogger) { Logger = new HeckLogger(pluginLogger); HeckData.InitPatches(_harmonyInstance, Assembly.GetExecutingAssembly()); Heck.Animation.TrackBuilder.TrackManagerCreated += Animation.AssignPlayerToTrack.OnTrackManagerCreated; Heck.Animation.TrackBuilder.TrackManagerCreated += Animation.AssignTrackParent.OnTrackManagerCreated; Heck.Animation.TrackBuilder.TrackCreated += Animation.AnimationHelper.OnTrackCreated; } [OnEnable] public void OnEnable() { SongCore.Collections.RegisterCapability(CAPABILITY); _harmonyInstanceCore.PatchAll(Assembly.GetExecutingAssembly()); } [OnDisable] public void OnDisable() { SongCore.Collections.DeregisterizeCapability(CAPABILITY); _harmonyInstanceCore.UnpatchAll(HARMONYIDCORE); _harmonyInstanceCore.UnpatchAll(HARMONYID); } } } ================================================ FILE: NoodleExtensions/manifest.json ================================================ { "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json", "author": "Aeroluna", "description": "This mod lets you see cool things that mappers have put in their maps. Report all issues to Reaxt.", "gameVersion": "1.18.0", "id": "NoodleExtensions", "name": "NoodleExtensions", "version": "1.4.4", "dependsOn": { "_Heck": "^1.3.0", "SongCore": "^3.8.0", "CustomJSONData": "^2.0.6", "BSIPA": "^4.2.0" } } ================================================ FILE: NoodleExtensions.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29025.244 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoodleExtensions", "NoodleExtensions\NoodleExtensions.csproj", "{6F22A5D9-D726-490F-A46C-B89CA4D76B41}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C716C2DD-39D3-4AC0-8B2D-E129E72F4828}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig README.md = README.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{3E8D1134-B4CE-458C-A21F-2016636840FF}" ProjectSection(SolutionItems) = preProject Documentation\AnimationDocs.md = Documentation\AnimationDocs.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{47ABF7CC-E146-49EA-8001-9236C121345F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "media", "media", "{78E1F3FF-36CF-4ECF-94D1-4011141FF22C}" ProjectSection(SolutionItems) = preProject Documentation\media\ColorAnimateTrack.gif = Documentation\media\ColorAnimateTrack.gif Documentation\media\ColorAssignPath.gif = Documentation\media\ColorAssignPath.gif Documentation\media\DefinitePositionAnimateTrack.gif = Documentation\media\DefinitePositionAnimateTrack.gif Documentation\media\DissolveAnimateTrack.gif = Documentation\media\DissolveAnimateTrack.gif Documentation\media\DissolveArrowAnimateTrack.gif = Documentation\media\DissolveArrowAnimateTrack.gif Documentation\media\DissolveArrowAssignPath.gif = Documentation\media\DissolveArrowAssignPath.gif Documentation\media\DissolveAssignPath.gif = Documentation\media\DissolveAssignPath.gif Documentation\media\LocalRotationAnimateTrack.gif = Documentation\media\LocalRotationAnimateTrack.gif Documentation\media\LocalRotationAssignPath.gif = Documentation\media\LocalRotationAssignPath.gif Documentation\media\PositionAnimateTrack1.gif = Documentation\media\PositionAnimateTrack1.gif Documentation\media\PositionAssignPath.gif = Documentation\media\PositionAssignPath.gif Documentation\media\RotationAnimateTrack.gif = Documentation\media\RotationAnimateTrack.gif Documentation\media\RotationAssignPath.gif = Documentation\media\RotationAssignPath.gif Documentation\media\TimeIsDumb.gif = Documentation\media\TimeIsDumb.gif EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "documentationMap", "documentationMap", "{4DDC5B47-1E50-463F-8C15-67CD182B795D}" ProjectSection(SolutionItems) = preProject Documentation\examples\documentationMap\cat.ogg = Documentation\examples\documentationMap\cat.ogg Documentation\examples\documentationMap\count.txt = Documentation\examples\documentationMap\count.txt Documentation\examples\documentationMap\demo.js = Documentation\examples\documentationMap\demo.js Documentation\examples\documentationMap\ExpertPlusStandard.dat = Documentation\examples\documentationMap\ExpertPlusStandard.dat Documentation\examples\documentationMap\ExpertStandard.dat = Documentation\examples\documentationMap\ExpertStandard.dat Documentation\examples\documentationMap\Info.dat = Documentation\examples\documentationMap\Info.dat Documentation\examples\documentationMap\README.md = Documentation\examples\documentationMap\README.md EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6F22A5D9-D726-490F-A46C-B89CA4D76B41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F22A5D9-D726-490F-A46C-B89CA4D76B41}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F22A5D9-D726-490F-A46C-B89CA4D76B41}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F22A5D9-D726-490F-A46C-B89CA4D76B41}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {3E8D1134-B4CE-458C-A21F-2016636840FF} = {C716C2DD-39D3-4AC0-8B2D-E129E72F4828} {47ABF7CC-E146-49EA-8001-9236C121345F} = {3E8D1134-B4CE-458C-A21F-2016636840FF} {78E1F3FF-36CF-4ECF-94D1-4011141FF22C} = {3E8D1134-B4CE-458C-A21F-2016636840FF} {4DDC5B47-1E50-463F-8C15-67CD182B795D} = {47ABF7CC-E146-49EA-8001-9236C121345F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C9ABA060-E44A-4177-B81E-E3E3784930C2} EndGlobalSection EndGlobal ================================================ FILE: README.md ================================================ **Noodle Extensions is now maintained in the [Aeroluna/Heck](https://github.com/Aeroluna/Heck) repository.** ---- # NoodleExtensions This adds a host of new things you can do with your maps. MAPPERS: EITHER USE MAPPING EXTENSIONS OR NOODLE EXTENSIONS, DO NOT USE BOTH AT THE SAME TIME. Noodle Extensions is meant to completely replace Mapping Extensions, as they both do the same thing. Having both requirements can break some features. NOODLE EXTENSIONS WILL NOT READ MAPS THAT USE MAPPING EXTENSIONS. YOU HAVE TO INSTALL MAPPING EXTENSIONS FOR THOSE. (You can have both of the mods installed at once) Documentation: https://github.com/Aeroluna/Heck/wiki