diff --git a/README.md b/README.md index bf0cb29..54b09df 100644 --- a/README.md +++ b/README.md @@ -31,17 +31,18 @@ Just go in the releases tab and download the latest version. ## Samples ```py from noodle_extensions import Editor, Animator +from noodle_extensions.constants import EventType, Animations editor = Editor("YourLevel.datPath") animator = Animator(editor) # Animations can go here. # Basic position animation (that does nothing) -animator.animate("AnimateTrack", "_position", [[0, 0]], "DummyTrach", 0, 3) +animator.animate(EventType().AnimateTrack, Animations().position, [[0, 0]], "DummyTrack", 0, 3) ``` ## Current Issues: -*There are currently no known issues.* +None #### Currently testing features (checked features have been tested and are working) * [X] Editor.updateDependencies * [X] Editor.editBlock @@ -50,6 +51,6 @@ animator.animate("AnimateTrack", "_position", [[0, 0]], "DummyTrach", 0, 3) * [X] Editor.getWall * [X] Editor.removeEvent * [X] Animator.animate -* [ ] Animator.animateBlock -* [ ] Animator.animateWall -* [ ] Animator.editTrack +* [X] Animator.animateBlock +* [X] Animator.animateWall +* [X] Animator.editTrack diff --git a/example/example_1.py b/example/example_1.py index 382ad69..86ef4ed 100644 --- a/example/example_1.py +++ b/example/example_1.py @@ -11,7 +11,7 @@ editor.editBlock(6, (1, 0), "BounceTrack") # Then we would animate it to do so! -animator.animate("AnimateTrack", Animations.position, [ +animator.animate("AnimateTrack", Animations().position, [ [0, 10, 0, 0], [0, 0, 0, 1, "easeOutBounce"] ], "BounceTrack", 5, 6) diff --git a/noodle_extension/__init__.py b/noodle_extensions/__init__.py similarity index 73% rename from noodle_extension/__init__.py rename to noodle_extensions/__init__.py index de4f755..598b49e 100644 --- a/noodle_extension/__init__.py +++ b/noodle_extensions/__init__.py @@ -21,7 +21,6 @@ import json, os from pathlib import Path from enum import Enum - from . import constants @@ -117,7 +116,7 @@ def updateDependencies(self, dependency:str) -> list: # warning: the next few lines are ugly. for _difficultyBeatmaps in infodat["_difficultyBeatmapSets"][x]["_difficultyBeatmaps"]: if _difficultyBeatmaps["_beatmapFilename"] == self.customLevelPath.split("\\")[len(self.customLevelPath.split("\\"))-1]: # if the difficulty is the same file as the one the user is using - if _difficultyBeatmaps["_customData"].get("_requirements") == None: + if _difficultyBeatmaps["_customData"].get("_requirements") is None: _difficultyBeatmaps["_customData"]["_requirements"] = [] if not dependency in _difficultyBeatmaps["_customData"]["_requirements"]: _difficultyBeatmaps["_customData"]["_requirements"].append(dependency) @@ -169,6 +168,7 @@ def editBlock(self, beat:int, pos:tuple, track:str=None, false:bool=False, inter } json.dump(notes, editnote_) return note + json.dump(notes, editnote_) def editWall(self, beat:int, length:int, index:int, track:str=None, false:bool=False, interactable:bool=True) -> dict: '''The exact same as EditNote except it's EditWall (edits a wall.) Returns the wall's data @@ -198,6 +198,7 @@ def editWall(self, beat:int, length:int, index:int, track:str=None, false:bool=F } json.dump(walls, EditWalls) return obst + json.dump(walls, EditWalls) def getBlock(self, beat:int, pos:tuple) -> dict: '''Returns a note's data - `beat` the beat at which the note can be found @@ -209,6 +210,7 @@ def getBlock(self, beat:int, pos:tuple) -> dict: for x in notes["_notes"]: if x["_time"] == beat and x["_lineIndex"] == pos[0] and x["_lineLayer"] == pos[1]: return x + raise ValueError("Could not find note") def getWall(self, beat:int, index:int, length:int) -> dict: '''Returns a wall's data. - `beat` the beat at which the wall starts. @@ -219,47 +221,53 @@ def getWall(self, beat:int, index:int, length:int) -> dict: notes = json.load(getNote) for x in notes["_obstacles"]: - if x["_time"] == beat and x["_lineIndex"] == index and x["_duration"] == length-beat: + if x["_time"] == beat and x["_lineIndex"] == index and x["_duration"] == length: return x - + raise ValueError("Could not find wall.") + def editEvent(self): # DEPRECATED + '''OBSOLETE / DEPRECATED + use removeEvent instead. (edit event is no longer supported) + ''' + def removeEvent(self, time:int, EventType:str, track:str, animationType:str=None) -> dict: + '''Removes an event from the `_customEvents` list. (Returns the removed event's data)\n + If there is more than just the `animationType` provided in the event, it will only remove the `animationType` property of the animation.\n + Otherwise, it will remove the entire event. + - `time` the time at which the custom event happens + - `EventType` the event type that will get removed. (constants.EventType) + - `track` the track of the event that's to be removed + - `animationType` the animation type to remove (constants.Animations) leave empty to remove entire event + ''' - def editEvent(self, time:int, EventType:str, track:str, editType:int, newData:dict=None): - '''Edits a specific customEvent. - - `time` the time at which the event occurs. - - `EventType` the type of the even you want to edit. - - `track` the track of the even you want to edit - - `editType` either constants.EditorEvent.remove, or constants.EditorEvent.change.\n - if using `constants.EditorEvent.remove`, then ignore the `newData` setting. + if EventType not in EVENTTYPES: + raise ValueError("EventTypes is invalid") + elif animationType not in ANIMATORTYPES and animationType is not None: + raise ValueError("animationType is invalid") + + with open(self.customLevelPath, 'r') as getData: + events = json.load(getData) + + with open(self.customLevelPath, 'w') as Remove: + + for x in events["_customData"]["_customEvents"]: + if x["_time"] == time and x["_type"] == EventType and x["_data"]["_track"] == track: + if animationType is not None: + if x["_data"].get(animationType) != None: + totals = 0 + for y in x.keys(): + totals = totals+1 if y in ANIMATORTYPES else totals + + if totals > 1 and animationType is not None: + del x["_data"][animationType] + json.dump(events, Remove) + return x + else: + events["_customData"]["_customEvents"].remove(x) + json.dump(events, Remove) + return x + json.dump(events, Remove) + + - - `newData` The new data of the event. If using `constants.EditorEvent.remove` then ignore this.\n - If not using `constants.EditorEvent.remove`, then insert the new data using `Animator.animate` as it returns the data you want. - ''' - with open(self.customLevelPath, 'r') as fd_get_events: - events:dict = json.load(fd_get_events) - - if editType != constants.EditorEvent.remove and newData is None: # if there's missing data and you're not removing - raise AttributeError("Missing newData setting, as you are not removing the event.") - - with open(self.customLevelPath, 'w') as fd_edit_events: - customEvents = events["_customData"]["_customEvents"] - if editType == constants.EditorEvent.remove: - for x in range(len(customEvents)): - if customEvents[x]["_type"] == EventType and customEvents[x]["_time"] == time and customEvents[x]["_data"]["_track"] == track: # a long line to just check whether or not the event is the correct one to remove. - customEvents.remove(customEvents[x]) - break - json.dump(events, fd_edit_events) - - elif editType == constants.EditorEvent.change: - for x in range(len(customEvents)): - if customEvents[x]["_type"] == EventType and customEvents[x]["_time"] == time and customEvents[x]["_data"]["_track"] == track: - customEvents[x] = newData - break - - elif editType == constants.EditorEvent.add: - for x in range(len(customEvents)): - if customEvents[x]["_type"] == EventType and customEvents[x]["_time"] == time and customEvents[x]["_data"]["_track"] == track: - customEvents[x]["_data"] = newData["_data"] - break class Animator: def __init__(self, editor:Editor): @@ -269,10 +277,11 @@ def __init__(self, editor:Editor): self.editor = editor # this is as to be able to access the actual level.dat file. - def animate(self, eventtype, animationType, data:list, track, start, end) -> dict: + def animate(self, eventtype:str, animationType:str, data:list, track:str, start:int, end:int) -> dict: '''Animates a block and returns the Event's dictionary. This doesn't support `AssignTrackParent` and `AssignPlayerToTrack`.\n - Instead, use `Animator.editTrack` + Instead, use `Animator.editTrack`\n + If you want to re-animate a track's certain property, this script will change it and not dupe it. - `data` (list) that should look something like this; - `eventtype` what kind of animation is this (NoodleExtensions.EVENTTYPES) @@ -287,7 +296,7 @@ def animate(self, eventtype, animationType, data:list, track, start, end) -> dic - `start` the start (in beats) where the animation should start - `end` the end (in beats) where the animation should end. ''' - + if animationType not in ANIMATORTYPES: raise IndexError(f"The provided animation type {animationType} is not valid.") @@ -298,86 +307,97 @@ def animate(self, eventtype, animationType, data:list, track, start, end) -> dic if animationType == "_color": # add chroma as a requirement if using _color self.editor.updateDependencies("Chroma") + with open(self.editor.customLevelPath, 'r') as GetCustomEvents: ce = json.load(GetCustomEvents) with open(self.editor.customLevelPath, 'w') as EditCustomEvents: - if ce["_customData"].get("_customEvents") == None: + if ce["_customData"].get("_customEvents") is None: ce["_customData"]["_customEvents"] = [] for event in ce["_customData"]["_customEvents"]: - if event["_data"].get(animationType) is not None: - if event["_data"][animationType] == data and event["_type"] == eventtype: # if that event already exists - json.dump(ce, EditCustomEvents) - return - - ce["_customData"]["_customEvents"].append( - { - "_time":start, - "_type":eventtype, - "_data":{ - "_track" : track, - "_duration": end-start, - animationType:data - } + if event["_data"]["_track"] == track and event["_type"] == eventtype and event["_time"] == start and event["_data"]["_duration"] == (end-start): + event["_data"][animationType] = data + json.dump(ce, EditCustomEvents) + return event + + newEvent = { + "_time": start, + "_type": eventtype, + "_data": { + "_track": track, + "_duration": end-start, + animationType:data } - ) + } + ce["_customData"]["_customEvents"].append(newEvent) json.dump(ce, EditCustomEvents) - return { - "_time":start, - "_type":eventtype, - "_data":{ - "_track" : track, - "_duration": end-start, - animationType:data - } - } - def animateBlock(self, beat, pos:tuple, animationType, data) -> dict: + return newEvent + + + def animateBlock(self, beat, pos:tuple, animationType:str, data) -> dict: '''Animate a specific note. (Returns the note's `_customData` property) - `beat` the beat at which the note can be found - `pos` The (x, y) position where the note is. (0, 0) is found left-most row, bottom layer. - `animationType` the property you want to animate. - `data` how the note should be animated ''' - if animationType not in ANIMATORTYPES: + + incorrect = True + for x in ANIMATORTYPES: + if x == animationType: + incorrect = False + break + if incorrect: raise ValueError("Incorrect animation type") + + with open(self.editor.customLevelPath, 'r') as getNote: + notes = json.load(getNote) + note = self.editor.getBlock(beat, pos) with open(self.editor.customLevelPath, 'w') as editNote: - notes = json.load(editNote) - note = self.editor.getBlock(beat, pos) - for x in notes["_notes"]: - if note == x: + for x in notes["_notes"]: + if x == note: + if x.get("_customData") is None: x["_customData"] = { animationType:data } - json.dump(notes, editNote) - note["_customData"] = x["_customData"] - break + else: + x["_customData"][animationType] = data + + json.dump(notes, editNote) + return x["_customData"] - return note["_customData"] - def animateWall(self, beat, length, index, animationType, data): + def animateWall(self, beat:int, length:int, index:int, animationType:str, data:list): '''Animates a specific wall. - `beat` the beat at which the wall starts. - `length` the length (in beat) of how long the wall is. - `index` ''' - if animationType not in ANIMATORTYPES: + incorrect = True + for x in ANIMATORTYPES: + if x == animationType: + incorrect = False + break + if incorrect: raise ValueError("Incorrect animation type") - with open(self.editor.customLevelPath, 'w') as editWall: - walls = json.load(editWall) - wall = self.editor.getWall(beat, index, length) + with open(self.editor.customLevelPath, 'r') as getWalls: + walls = json.load(getWalls) + + wall = self.editor.getWall(beat, index, length) + with open(self.editor.customLevelPath, 'w') as editWalls: for x in walls["_obstacles"]: if x == wall: - x["_customData"] = { - animationType:data - } - json.dump(walls, editWall) - wall["_customData"] = x["_customData"] - break - - return wall["_customData"] + if x.get("_customData") is None: + x["_customData"] = { + animationType: data + } + else: + x["_customData"][animationType] = data + json.dump(walls, editWalls) + return x["_customData"] def editTrack(self, eventType, time, tracks, parentTrack:str=None) -> dict: '''Edit Track allows you to either do `AssignTrackParent` or `AssignPlayerToTrack` and returns the event - `eventType` Either `AssignTrackParent` or `AssignPlayerToTrack` @@ -403,11 +423,11 @@ def editTrack(self, eventType, time, tracks, parentTrack:str=None) -> dict: events = json.load(getEvents) with open(self.editor.customLevelPath, 'w') as editEvents: - for customs in events["_customEvents"]: + for customs in events["_customData"]["_customEvents"]: if customs == event: # if the event already exists - return - events["_customEvents"].append(event) - json.dump(events, editEvents) + json.dump(events, editEvents) + return event + events["_customData"]["_customEvents"].append(event) return event elif eventType == "AssignPlayerToTrack": @@ -423,8 +443,10 @@ def editTrack(self, eventType, time, tracks, parentTrack:str=None) -> dict: events = json.load(getEvents) with open(self.editor.customLevelPath, 'w') as editEvents: - for customs in events["_customEvents"]: + for customs in events["_customData"]["_customEvents"]: if customs == event: - return - events["_customEvents"].append(event) + json.dump(events, editEvents) + return event + events["_customData"]["_customEvents"].append(event) json.dump(events, editEvents) + return event diff --git a/noodle_extension/constants.py b/noodle_extensions/constants.py similarity index 71% rename from noodle_extension/constants.py rename to noodle_extensions/constants.py index 595e5ff..761e2a9 100644 --- a/noodle_extension/constants.py +++ b/noodle_extensions/constants.py @@ -1,6 +1,6 @@ from enum import Enum -class Animations(Enum): +class Animations: position = "_position" rotation = "_rotation" localRotation = "_localRotation" @@ -10,13 +10,8 @@ class Animations(Enum): time = "_time" color = "_color" -class AnimationTypes(Enum): +class EventType: AnimateTrack = "AnimateTrack" AssignPathAnimation = "AssignPathAnimation" AssignTrackParent = "AssignTrackParent" - AssignPlayerToTrack = "AssignPlayerToTrack" - -class EditorEvent(Enum): - remove = 0 - change = 1 - add = 2 + AssignPlayerToTrack = "AssignPlayerToTrack" \ No newline at end of file diff --git a/test_animator.py b/test_animator.py new file mode 100644 index 0000000..55bb4f9 --- /dev/null +++ b/test_animator.py @@ -0,0 +1,54 @@ +import sys + +from noodle_extensions import Editor, Animator +from noodle_extensions.constants import EventType, Animations + +editor = Editor(r"C:\Program Files (x86)\Steam\steamapps\common\Beat Saber\Beat Saber_Data\CustomWIPLevels\ExampleLevel\EasyStandard.dat") +animator = Animator(editor) + + +def test_animate(): + assert animator.animate("AnimateTrack", "_position", [[0, 0]], "DummyTrach", 0, 3) == { + "_time": 0, + "_type": "AnimateTrack", + "_data": { + "_track": "DummyTrach", + "_duration": 3, + "_position": [ + [ + 0, + 0 + ] + ] + } + } + +def test_animateBlock(): + # pos = Animations().position + assert animator.animateBlock(14, (2, 0), Animations().position, [[0, 0]]).get("_position") is not None + +def test_animateWall(): + assert animator.animateWall(18, 6, 3, Animations().position, [[0, 0]]).get("_position") is not None + +def test_editTrack(): + #AssignPlayerToTrack + data = animator.editTrack("AssignPlayerToTrack", 5, "TestTrack") + assert data == { + "_time":5, + "_type":"AssignPlayerToTrack", + "_data":{ + "_track":"TestTrack" + } + } + +def test_editTrack2(): + #AssignTrackParent + data = animator.editTrack("AssignTrackParent", 5, "TestTrack", "ParentTrack") + assert data == { + "_time":5, + "_type":"AssignTrackParent", + "_data":{ + "_childrenTracks":["TestTrack"], + "_parentTrack":"ParentTrack" + } + } \ No newline at end of file diff --git a/tests/test_init.py b/tests/test_init.py deleted file mode 100644 index 3a1020b..0000000 --- a/tests/test_init.py +++ /dev/null @@ -1,81 +0,0 @@ -import sys - -from noodle_extensions import Animator, Editor - -editor = Editor(r"C:\Program Files (x86)\Steam\steamapps\common\Beat Saber\Beat Saber_Data\CustomWIPLevels\ExampleLevel\EasyStandard.dat") -animator = Animator(editor) - - -# Editor tests -def test_UpdateReqs(): - assert "Noodle Extensions" in editor.updateDependencies("Noodle Extensions") - -def test_editNote(): - beat = 14 - pos = (2,0) - track = "TestTrack" - assert editor.editBlock(beat, pos, track) == { - "_time": 14, - "_lineIndex": 2, - "_lineLayer": 0, - "_type": 1, - "_cutDirection": 1, - "_customData" : { - "_track":track, - "_fake":False, - "_interactable":True - } - } -def test_editWall(): - beat = 18 - length = 18+6 # start beat + length of wall - index = 3 - assert editor.editWall(beat, length, index, "TestTrack") == { - - "_time": 18, - "_lineIndex": 3, - "_type": 0, - "_duration": 6, - "_width": 1, - "_customData": { - "_track": "TestTrack", - "_fake": False, - "_interactable": True - } - } - -def test_getBlock(): - beat = 14 - pos = (2,0) - track = "TestTrack" - block = editor.getBlock(beat, pos) - assert block == { - "_time": 14, - "_lineIndex": 2, - "_lineLayer": 0, - "_type": 1, - "_cutDirection": 1, - "_customData" : { - "_track":track, - "_fake":False, - "_interactable":True - } - } - -def test_getWall(): - beat = 18 - length = 18+6 # start beat + length of wall - index = 3 - wall = editor.getWall(beat, index, length) - assert wall == { - "_time": 18, - "_lineIndex": 3, - "_type": 0, - "_duration": 6, - "_width": 1, - "_customData": { - "_track": "TestTrack", - "_fake": False, - "_interactable": True - } - } \ No newline at end of file