1- from beet import Context , JsonFile , Function , NamespaceFileScope
1+ from beet import Context , JsonFile , Function , NamespaceFileScope # pyright: ignore[reportMissingImports]
2+ from pydantic import BaseModel # pyright: ignore[reportMissingImports]
3+ from typing import Tuple , Dict , Any , List , Optional , ClassVar
24from pathlib import Path
35import json
46import copy
5- from typing import Tuple , Dict , Any , List , Optional , ClassVar
6- from pydantic import BaseModel
77
88TICK_FACTOR : int = 3
99TICK_OFFSET : int = - 6000
10- FOLDER_PATH : str = "gm4_timelines/raw_data/"
11-
10+ DAY_DURATION_TICKS = 24000
11+ FOLDER_PATH : Path = Path ("gm4_timelines/raw_data" )
12+ SLIME_VALUES = {
13+ "full_moon" : 0.5 ,
14+ "waning_gibbous" : 0.375 ,
15+ "third_quarter" : 0.25 ,
16+ "waning_crescent" : 0.125 ,
17+ "new_moon" : 0 ,
18+ "waxing_crescent" : 0.125 ,
19+ "first_quarter" : 0.25 ,
20+ "waxing_gibbous" : 0.375 ,
21+ }
1222
1323# ------------------
1424# - PYDANTIC STUFF -
@@ -46,17 +56,18 @@ class Timeline(JsonFile):
4656# -------------
4757
4858def beet_default (ctx : Context ) -> None :
59+ # We register Timeline to Beet here since it doesn't support it yet, when it does this can be removed
4960 ctx .data .extend_namespace += [Timeline ]
5061
51- vanilla_folder = Path ( FOLDER_PATH ) / "vanilla"
62+ vanilla_folder = FOLDER_PATH / "vanilla"
5263
5364 for file_path in vanilla_folder .glob ("*.json" ):
5465 with file_path .open ("r" , encoding = "utf-8" ) as f :
5566 data_dict : Dict [str , Any ] = json .load (f )
5667 data = TimelineData (** data_dict )
5768
5869 # factor and offset the ticks so it matches the new daytime
59- data = factor_ticks (ctx , data )
70+ data = factor_ticks (data )
6071
6172 file_name = file_path .stem
6273 if file_name == "day" :
@@ -66,97 +77,133 @@ def beet_default(ctx: Context) -> None:
6677
6778
6879
69- def factor_ticks (ctx : Context , data : TimelineData ) -> TimelineData :
80+ def factor_ticks (data : TimelineData ) -> TimelineData :
7081 """
82+ Each keyframe tick is shifted by a fixed offset, wrapped to the original
83+ period, and then multiplied by the tick factor. The timeline period
84+ itself is also scaled accordingly.
85+
86+ Args:
87+ data: Timeline data whose ticks will be modified.
88+
89+ Returns:
90+ TimelineData instance with modified tick values.
7191 """
72- period = data .period_ticks
92+ factored_data = copy .deepcopy (data )
93+ period = factored_data .period_ticks
7394
7495 # offset and scale the ticks
75- for track in data .tracks .values ():
96+ for track in factored_data .tracks .values ():
7697 for kf in track .keyframes :
7798 kf .ticks = ((kf .ticks + TICK_OFFSET ) % period ) * TICK_FACTOR
7899 track .keyframes .sort (key = lambda k : k .ticks )
79100
80101 # scale the period
81- data .period_ticks = period * TICK_FACTOR
102+ factored_data .period_ticks = period * TICK_FACTOR
82103
83- return data
104+ return factored_data
84105
85106
86107
87108def register_days (ctx : Context , data : TimelineData ) -> TimelineData :
88109 """
89- """
90- build_timeline = TimelineData (period_ticks = 0 , tracks = {})
110+ Load day definition files from either dev or days folder. Build combined
111+ timeline by concatenating the tracks of each day's timeline
112+
113+ Args:
114+ data: Base timeline used as the default state for each generated day.
91115
92- dev_folder = Path (FOLDER_PATH ) / "dev"
116+ Returns:
117+ TimelineData instance representing the concatenated day timeline.
118+ """
119+ dev_folder = FOLDER_PATH / "dev"
93120 dev_files = list (dev_folder .glob ("*.json" ))
94121
95122 if dev_files :
96- build_timeline , function_data = process_day_files (ctx , dev_folder , build_timeline , data , is_dev = True )
123+ return process_day_files (ctx , dev_folder , data , is_dev = True )
97124 else :
98- days_folder = Path (FOLDER_PATH ) / "days"
99- build_timeline , function_data = process_day_files (ctx , days_folder , build_timeline , data )
100-
101- # create function
102- day_durarion = 24000 * TICK_FACTOR
103- ctx .data ["gm4_timelines:register/days" ] = Function ([
104- "# register days" ,
105- "# generated from generate.py" ,
106- "" ,
107- f'data modify storage gm4_timelines:data day_duration set value { day_durarion } ' ,
108- f'data modify storage gm4_timelines:data day_registry set value { function_data } ' ,
109- ])
110-
111- return build_timeline
125+ days_folder = FOLDER_PATH / "days"
126+ return process_day_files (ctx , days_folder , data )
112127
113128
114129
115130def process_day_files (
116131 ctx : Context ,
117132 folder_path : Path ,
118- build_timeline : TimelineData ,
119- data : TimelineData ,
133+ default_data : TimelineData ,
120134 is_dev : bool = False
121- ) -> Tuple [ TimelineData , list [ Any ]] :
135+ ) -> TimelineData :
122136 """
137+ Load each day definition from provided folder and build a timeline for them.
138+ Days are added once per supported moon phase. Also collects metadata needed to
139+ register the day into storage for the datapack.
140+
141+ Args:
142+ folder_path: Directory containing day definition JSON files.
143+ default_data: Default timeline used as a base for each day.
144+ is_dev (default = False): Whether the day data comes from the development folder.
145+
146+ Returns:
147+ TimelineData instance representing the concatenated day timeline.
123148 """
124- function_data : list [Any ] = []
149+ function_data : List [Dict [str , Any ]] = []
150+ full_timeline = TimelineData (period_ticks = 0 , tracks = {})
125151
152+ # loop over day definitions
126153 for file_path in folder_path .glob ("*.json" ):
127154 with file_path .open ("r" , encoding = "utf-8" ) as f :
128155 day_data_dict = json .load (f )
129156 day_data = DayData (** day_data_dict )
130157
131- moon_phases = day_data . settings [ 'moon_phase' ]
132-
158+ # loop over supported moon phases
159+ moon_phases = day_data . settings . get ( "moon_phase" , [])
133160 for moon_phase in moon_phases :
134- day_build , functions = register_day (ctx , data , day_data , moon_phase )
135- build_timeline = append_to_timeline (build_timeline , day_build )
161+ day_build , functions = register_day (default_data , day_data , moon_phase )
162+ full_timeline = append_to_timeline (full_timeline , day_build )
136163
137164 function_data .append ({
138165 "moon_phase" : moon_phase ,
139166 "in_type" : day_data .settings ['in_type' ],
140167 "out_type" : day_data .settings ['out_type' ],
141168 "weight" : day_data .settings ['weight' ],
142- "start_time" : build_timeline .period_ticks ,
169+ "start_time" : full_timeline .period_ticks ,
143170 "functions" : functions ,
144171 "dev" : is_dev
145172 })
146173
147- build_timeline .period_ticks += day_build .period_ticks
174+ full_timeline .period_ticks += day_build .period_ticks
148175
149- return build_timeline , function_data
176+ # create function
177+ day_duration = DAY_DURATION_TICKS * TICK_FACTOR
178+ ctx .data ["gm4_timelines:register/days" ] = Function ([
179+ "# register days" ,
180+ "# generated from generate.py" ,
181+ "" ,
182+ f'data modify storage gm4_timelines:data day_duration set value { day_duration } ' ,
183+ f'data modify storage gm4_timelines:data day_registry set value { function_data } ' ,
184+ ])
185+
186+ return full_timeline
150187
151188
152189
153190def register_day (
154- ctx : Context ,
155191 default_data : TimelineData ,
156192 day_data : DayData ,
157193 moon_phase : str
158194 ) -> Tuple [TimelineData , List [Dict [str , Any ]]]:
159195 """
196+ Build the day data for a day definition, add moon phase and slime spawn chance
197+ tracks and get scheduled function calls. Any non-modified tracks are taken from default_data
198+
199+ Args:
200+ default_data: Base timeline providing default tracks and values.
201+ day_data: Parsed day configuration and schedule.
202+ moon_phase: Moon phase identifier for this day variant.
203+
204+ Returns:
205+ A tuple containing the generated day timeline and a list of function
206+ calls to be triggered at specific ticks.
160207 """
161208 result = TimelineData (period_ticks = default_data .period_ticks , tracks = {})
162209 seen_tracks : set [str ] = set ()
@@ -170,17 +217,7 @@ def register_day(
170217 seen_tracks .add (track_name )
171218
172219 # create the surface slime spawn chance track
173- slime_values = {
174- "full_moon" : 0.5 ,
175- "waning_gibbous" : 0.375 ,
176- "third_quarter" : 0.25 ,
177- "waning_crescent" : 0.125 ,
178- "new_moon" : 0 ,
179- "waxing_crescent" : 0.125 ,
180- "first_quarter" : 0.25 ,
181- "waxing_gibbous" : 0.375 ,
182- }
183- slime_value = slime_values .get (moon_phase , 0.0 )
220+ slime_value = SLIME_VALUES .get (moon_phase , 0.0 )
184221 track_name = "minecraft:gameplay/surface_slime_spawn_chance"
185222 result .tracks [track_name ] = Track (
186223 keyframes = [Keyframe (ticks = 0 , value = slime_value )],
@@ -194,9 +231,11 @@ def register_day(
194231 ticks = entry .time
195232 effects = entry .effects
196233
197- if entry .functions != []:
234+ # register function calls
235+ if entry .functions :
198236 functions .append ({"tick" :ticks ,"functions" :entry .functions })
199237
238+ # transform effects into timeline tracks
200239 for effect_name , value in effects .items ():
201240 track_name = f"minecraft:{ effect_name } "
202241 seen_tracks .add (track_name )
@@ -207,39 +246,51 @@ def register_day(
207246 track = Track (
208247 keyframes = [],
209248 modifier = default_track .modifier if default_track else None ,
210- ease = copy . deepcopy ( default_track .ease ) if default_track else None
249+ ease = default_track .ease if default_track else None
211250 )
212251 result .tracks [track_name ] = track
213252
214- track .keyframes .append (Keyframe (ticks = ticks , value = copy . deepcopy ( value ) ))
253+ track .keyframes .append (Keyframe (ticks = ticks , value = value ))
215254
216- # Copy over untouched default tracks
255+ # copy over untouched default tracks
217256 for track_name , track_data in default_data .tracks .items ():
218257 if track_name not in seen_tracks :
219258 result .tracks [track_name ] = copy .deepcopy (track_data )
220259
221260 return result , functions
222261
223262
224- def append_to_timeline (build_timeline : TimelineData , new_data : TimelineData ) -> TimelineData :
263+ def append_to_timeline (full_timeline : TimelineData , new_data : TimelineData ) -> TimelineData :
225264 """
265+ Append the next day timeline into the full timeline by offsetting its ticks, they are
266+ shifted by the current period of the full timeline and then merged into the matching
267+ tracks.
268+
269+ Args:
270+ full_timeline: The timeline being appended to.
271+ new_data: The timeline segment to append.
272+
273+ Returns:
274+ The build timeline with the new timeline data merged in.
226275 """
227- offset = build_timeline .period_ticks
276+ offset = full_timeline .period_ticks
228277
229278 for track_name , src_track in new_data .tracks .items ():
230- dst_track = build_timeline .tracks .get (track_name )
231279
280+ # find matching track, or create if neccesary
281+ dst_track = full_timeline .tracks .get (track_name )
232282 if not dst_track :
233283 dst_track = Track (
234284 keyframes = [],
235285 modifier = src_track .modifier ,
236286 ease = copy .deepcopy (src_track .ease )
237287 )
238- build_timeline .tracks [track_name ] = dst_track
288+ full_timeline .tracks [track_name ] = dst_track
239289
290+ # append the keyframes, adding the offset to each ticks value
240291 for kf in src_track .keyframes :
241292 dst_track .keyframes .append (
242293 Keyframe (ticks = kf .ticks + offset , value = copy .deepcopy (kf .value ))
243294 )
244295
245- return build_timeline
296+ return full_timeline
0 commit comments