· 8 years ago · Jun 09, 2018, 05:02 PM
1-------------------------------------------------------------------------------------------------------------------
2-- General utility functions that can be used by any job files.
3-- Outside the scope of what the main include file deals with.
4-- Modified JUN 10 2018
5-------------------------------------------------------------------------------------------------------------------
6
7-------------------------------------------------------------------------------------------------------------------
8-- Buff utility functions.
9-------------------------------------------------------------------------------------------------------------------
10
11local cancel_spells_to_check = S{'Sneak', 'Stoneskin', 'Spectral Jig', 'Trance', 'Monomi: Ichi', 'Utsusemi: Ichi'}
12local cancel_types_to_check = S{'Waltz', 'Samba'}
13
14-- Function to cancel buffs if they'd conflict with using the spell you're attempting.
15-- Requirement: Must have Cancel addon installed and loaded for this to work.
16function cancel_conflicting_buffs(spell, action, spellMap, eventArgs)
17 if cancel_spells_to_check:contains(spell.english) or cancel_types_to_check:contains(spell.type) then
18 if spell.action_type == 'Ability' then
19 local abil_recasts = windower.ffxi.get_ability_recasts()
20 if abil_recasts[spell.recast_id] > 0 then
21 add_to_chat(123,'Abort: Ability waiting on recast.')
22 eventArgs.cancel = true
23 return
24 end
25 elseif spell.action_type == 'Magic' then
26 local spell_recasts = windower.ffxi.get_spell_recasts()
27 if spell_recasts[spell.recast_id] > 0 then
28 add_to_chat(123,'Abort: Spell waiting on recast.')
29 eventArgs.cancel = true
30 return
31 end
32 end
33
34 if spell.english == 'Spectral Jig' and buffactive.sneak then
35 cast_delay(0.2)
36 send_command('cancel sneak')
37 elseif spell.english == 'Sneak' and spell.target.type == 'SELF' and buffactive.sneak then
38 send_command('cancel sneak')
39 elseif spell.english == ('Stoneskin') then
40 send_command('@wait 1.0;cancel stoneskin')
41 elseif spell.english:startswith('Monomi') then
42 send_command('@wait 1.7;cancel sneak')
43 elseif spell.english == 'Utsusemi: Ichi' then
44 send_command('@wait 1.7;cancel copy image,copy image (2)')
45 elseif (spell.english == 'Trance' or spell.type=='Waltz') and buffactive['saber dance'] then
46 cast_delay(0.2)
47 send_command('cancel saber dance')
48 elseif spell.type=='Samba' and buffactive['fan dance'] then
49 cast_delay(0.2)
50 send_command('cancel fan dance')
51 end
52 end
53end
54
55
56-- Some mythics have special durations for level 1 and 2 aftermaths
57local special_aftermath_mythics = S{'Tizona', 'Kenkonken', 'Murgleis', 'Yagrush', 'Carnwenhan', 'Nirvana', 'Tupsimati', 'Idris'}
58
59-- Call from job_precast() to setup aftermath information for custom timers.
60function custom_aftermath_timers_precast(spell)
61 if spell.type == 'WeaponSkill' then
62 info.aftermath = {}
63
64 local relic_ws = data.weaponskills.relic[player.equipment.main] or data.weaponskills.relic[player.equipment.range]
65 local mythic_ws = data.weaponskills.mythic[player.equipment.main] or data.weaponskills.mythic[player.equipment.range]
66 local empy_ws = data.weaponskills.empyrean[player.equipment.main] or data.weaponskills.empyrean[player.equipment.range]
67
68 if not relic_ws and not mythic_ws and not empy_ws then
69 return
70 end
71
72 info.aftermath.weaponskill = spell.english
73 info.aftermath.duration = 0
74
75 info.aftermath.level = math.floor(player.tp / 1000)
76 if info.aftermath.level == 0 then
77 info.aftermath.level = 1
78 end
79
80 if spell.english == relic_ws then
81 info.aftermath.duration = math.floor(0.2 * player.tp)
82 if info.aftermath.duration < 20 then
83 info.aftermath.duration = 20
84 end
85 elseif spell.english == empy_ws then
86 -- nothing can overwrite lvl 3
87 if buffactive['Aftermath: Lv.3'] then
88 return
89 end
90 -- only lvl 3 can overwrite lvl 2
91 if info.aftermath.level ~= 3 and buffactive['Aftermath: Lv.2'] then
92 return
93 end
94
95 -- duration is based on aftermath level
96 info.aftermath.duration = 30 * info.aftermath.level
97 elseif spell.english == mythic_ws then
98 -- nothing can overwrite lvl 3
99 if buffactive['Aftermath: Lv.3'] then
100 return
101 end
102 -- only lvl 3 can overwrite lvl 2
103 if info.aftermath.level ~= 3 and buffactive['Aftermath: Lv.2'] then
104 return
105 end
106
107 -- Assume mythic is lvl 80 or higher, for duration
108
109 if info.aftermath.level == 1 then
110 info.aftermath.duration = (special_aftermath_mythics:contains(player.equipment.main) and 270) or 90
111 elseif info.aftermath.level == 2 then
112 info.aftermath.duration = (special_aftermath_mythics:contains(player.equipment.main) and 270) or 120
113 else
114 info.aftermath.duration = 180
115 end
116 end
117 end
118end
119
120
121-- Call from job_aftercast() to create the custom aftermath timer.
122function custom_aftermath_timers_aftercast(spell)
123 if not spell.interrupted and spell.type == 'WeaponSkill' and
124 info.aftermath and info.aftermath.weaponskill == spell.english and info.aftermath.duration > 0 then
125
126 local aftermath_name = 'Aftermath: Lv.'..tostring(info.aftermath.level)
127 send_command('timers d "Aftermath: Lv.1"')
128 send_command('timers d "Aftermath: Lv.2"')
129 send_command('timers d "Aftermath: Lv.3"')
130 send_command('timers c "'..aftermath_name..'" '..tostring(info.aftermath.duration)..' down abilities/00027.png')
131
132 info.aftermath = {}
133 end
134end
135
136
137-------------------------------------------------------------------------------------------------------------------
138-- Utility functions for changing spells and target types in an automatic manner.
139-------------------------------------------------------------------------------------------------------------------
140
141local waltz_tp_cost = {['Curing Waltz'] = 200, ['Curing Waltz II'] = 350, ['Curing Waltz III'] = 500, ['Curing Waltz IV'] = 650, ['Curing Waltz V'] = 800}
142
143-- Utility function for automatically adjusting the waltz spell being used to match HP needs and TP limits.
144-- Handle spell changes before attempting any precast stuff.
145function refine_waltz(spell, action, spellMap, eventArgs)
146 if spell.type ~= 'Waltz' then
147 return
148 end
149
150 -- Don't modify anything for Healing Waltz or Divine Waltzes
151 if spell.english == "Healing Waltz" or spell.english == "Divine Waltz" or spell.english == "Divine Waltz II" then
152 return
153 end
154
155 local newWaltz = spell.english
156 local waltzID
157
158 local missingHP
159
160 -- If curing ourself, get our exact missing HP
161 if spell.target.type == "SELF" then
162 missingHP = player.max_hp - player.hp
163 -- If curing someone in our alliance, we can estimate their missing HP
164 elseif spell.target.isallymember then
165 local target = find_player_in_alliance(spell.target.name)
166 local est_max_hp = target.hp / (target.hpp/100)
167 missingHP = math.floor(est_max_hp - target.hp)
168 end
169
170 -- If we have an estimated missing HP value, we can adjust the preferred tier used.
171 if missingHP ~= nil then
172 if player.main_job == 'DNC' then
173 if missingHP < 40 and spell.target.name == player.name then
174 -- Not worth curing yourself for so little.
175 -- Don't block when curing others to allow for waking them up.
176 add_to_chat(122,'Full HP!')
177 eventArgs.cancel = true
178 return
179 elseif missingHP < 200 then
180 newWaltz = 'Curing Waltz'
181 waltzID = 190
182 elseif missingHP < 600 then
183 newWaltz = 'Curing Waltz II'
184 waltzID = 191
185 elseif missingHP < 1100 then
186 newWaltz = 'Curing Waltz III'
187 waltzID = 192
188 elseif missingHP < 1500 then
189 newWaltz = 'Curing Waltz IV'
190 waltzID = 193
191 else
192 newWaltz = 'Curing Waltz V'
193 waltzID = 311
194 end
195 elseif player.sub_job == 'DNC' then
196 if missingHP < 40 and spell.target.name == player.name then
197 -- Not worth curing yourself for so little.
198 -- Don't block when curing others to allow for waking them up.
199 add_to_chat(122,'Full HP!')
200 eventArgs.cancel = true
201 return
202 elseif missingHP < 150 then
203 newWaltz = 'Curing Waltz'
204 waltzID = 190
205 elseif missingHP < 300 then
206 newWaltz = 'Curing Waltz II'
207 waltzID = 191
208 else
209 newWaltz = 'Curing Waltz III'
210 waltzID = 192
211 end
212 else
213 -- Not dnc main or sub; bail out
214 return
215 end
216 end
217
218 local tpCost = waltz_tp_cost[newWaltz]
219
220 local downgrade
221
222 -- Downgrade the spell to what we can afford
223 if player.tp < tpCost and not buffactive.trance then
224 --[[ Costs:
225 Curing Waltz: 200 TP
226 Curing Waltz II: 350 TP
227 Curing Waltz III: 500 TP
228 Curing Waltz IV: 650 TP
229 Curing Waltz V: 800 TP
230 Divine Waltz: 400 TP
231 Divine Waltz II: 800 TP
232 --]]
233
234 if player.tp < 200 then
235 add_to_chat(122, 'Insufficient TP ['..tostring(player.tp)..']. Cancelling.')
236 eventArgs.cancel = true
237 return
238 elseif player.tp < 350 then
239 newWaltz = 'Curing Waltz'
240 elseif player.tp < 500 then
241 newWaltz = 'Curing Waltz II'
242 elseif player.tp < 650 then
243 newWaltz = 'Curing Waltz III'
244 elseif player.tp < 800 then
245 newWaltz = 'Curing Waltz IV'
246 end
247
248 downgrade = 'Insufficient TP ['..tostring(player.tp)..']. Downgrading to '..newWaltz..'.'
249 end
250
251
252 if newWaltz ~= spell.english then
253 send_command('@input /ja "'..newWaltz..'" '..tostring(spell.target.raw))
254 if downgrade then
255 add_to_chat(122, downgrade)
256 end
257 eventArgs.cancel = true
258 return
259 end
260
261 if missingHP and missingHP > 0 then
262 add_to_chat(122,'Trying to cure '..tostring(missingHP)..' HP using '..newWaltz..'.')
263 end
264end
265
266
267-- Function to allow for automatic adjustment of the spell target type based on preferences.
268function auto_change_target(spell, spellMap)
269 -- Don't adjust targetting for explicitly named targets
270 if not spell.target.raw:startswith('<') then
271 return
272 end
273
274 -- Do not modify target for spells where we get <lastst> or <me>.
275 if spell.target.raw == ('<lastst>') or spell.target.raw == ('<me>') then
276 return
277 end
278
279 -- init a new eventArgs with current values
280 local eventArgs = {handled = false, PCTargetMode = state.PCTargetMode.value, SelectNPCTargets = state.SelectNPCTargets.value}
281
282 -- Allow the job to do custom handling, or override the default values.
283 -- They can completely handle it, or set one of the secondary eventArgs vars to selectively
284 -- override the default state vars.
285 if job_auto_change_target then
286 job_auto_change_target(spell, action, spellMap, eventArgs)
287 end
288
289 -- If the job handled it, we're done.
290 if eventArgs.handled then
291 return
292 end
293
294 local pcTargetMode = eventArgs.PCTargetMode
295 local selectNPCTargets = eventArgs.SelectNPCTargets
296
297
298 local validPlayers = S{'Self', 'Player', 'Party', 'Ally', 'NPC'}
299
300 local intersection = spell.targets * validPlayers
301 local canUseOnPlayer = not intersection:empty()
302
303 local newTarget
304
305 -- For spells that we can cast on players:
306 if canUseOnPlayer and pcTargetMode ~= 'default' then
307 -- Do not adjust targetting for player-targettable spells where the target was <t>
308 if spell.target.raw ~= ('<t>') then
309 if pcTargetMode == 'stal' then
310 -- Use <stal> if possible, otherwise fall back to <stpt>.
311 if spell.targets.Ally then
312 newTarget = '<stal>'
313 elseif spell.targets.Party then
314 newTarget = '<stpt>'
315 end
316 elseif pcTargetMode == 'stpt' then
317 -- Even ally-possible spells are limited to the current party.
318 if spell.targets.Ally or spell.targets.Party then
319 newTarget = '<stpt>'
320 end
321 elseif pcTargetMode == 'stpc' then
322 -- If it's anything other than a self-only spell, can change to <stpc>.
323 if spell.targets.Player or spell.targets.Party or spell.targets.Ally or spell.targets.NPC then
324 newTarget = '<stpc>'
325 end
326 end
327 end
328 -- For spells that can be used on enemies:
329 elseif spell.targets and spell.targets.Enemy and selectNPCTargets then
330 -- Note: this means macros should be written for <t>, and it will change to <stnpc>
331 -- if the flag is set. It won't change <stnpc> back to <t>.
332 newTarget = '<stnpc>'
333 end
334
335 -- If a new target was selected and is different from the original, call the change function.
336 if newTarget and newTarget ~= spell.target.raw then
337 change_target(newTarget)
338 end
339end
340
341
342-------------------------------------------------------------------------------------------------------------------
343-- Environment utility functions.
344-------------------------------------------------------------------------------------------------------------------
345
346-- Function to get the current weather intensity: 0 for none, 1 for single weather, 2 for double weather.
347function get_weather_intensity()
348 return gearswap.res.weather[world.weather_id].intensity
349end
350
351
352-- Returns true if you're in a party solely comprised of Trust NPCs.
353-- TODO: Do we need a check to see if we're in a party partly comprised of Trust NPCs?
354function is_trust_party()
355 -- Check if we're solo
356 if party.count == 1 then
357 return false
358 end
359
360 -- If we're in an alliance, can't be a Trust party.
361 if alliance[2].count > 0 or alliance[3].count > 0 then
362 return false
363 end
364
365 -- Check that, for each party position aside from our own, the party
366 -- member has one of the Trust NPC names, and that those party members
367 -- are flagged is_npc.
368 for i = 2,6 do
369 if party[i] then
370 if not npcs.Trust:contains(party[i].name) then
371 return false
372 end
373 if party[i].mob and party[i].mob.is_npc == false then
374 return false
375 end
376 end
377 end
378
379 -- If it didn't fail any of the above checks, return true.
380 return true
381end
382
383
384-- Call these function with a list of equipment slots to check ('head', 'neck', 'body', etc)
385-- Returns true if any of the specified slots are currently encumbered.
386-- Returns false if all specified slots are unencumbered.
387function is_encumbered(...)
388 local check_list = {...}
389 -- Compensate for people passing a table instead of a series of strings.
390 if type(check_list[1]) == 'table' then
391 check_list = check_list[1]
392 end
393 local check_set = S(check_list)
394
395 for slot_id,slot_name in pairs(gearswap.default_slot_map) do
396 if check_set:contains(slot_name) then
397 if gearswap.encumbrance_table[slot_id] then
398 return true
399 end
400 end
401 end
402
403 return false
404end
405
406-------------------------------------------------------------------------------------------------------------------
407-- Elemental gear utility functions.
408-------------------------------------------------------------------------------------------------------------------
409
410-- General handler function to set all the elemental gear for an action.
411function set_elemental_gear(spell)
412 set_elemental_gorget_belt(spell)
413 set_elemental_obi_cape_ring(spell)
414 set_elemental_staff(spell)
415end
416
417
418-- Set the name field of the predefined gear vars for gorgets and belts, for the specified weaponskill.
419function set_elemental_gorget_belt(spell)
420 if spell.type ~= 'WeaponSkill' then
421 return
422 end
423
424 -- Get the union of all the skillchain elements for the weaponskill
425 local weaponskill_elements = S{}:
426 union(skillchain_elements[spell.skillchain_a]):
427 union(skillchain_elements[spell.skillchain_b]):
428 union(skillchain_elements[spell.skillchain_c])
429
430 gear.ElementalGorget.name = get_elemental_item_name("gorget", weaponskill_elements) or gear.default.weaponskill_neck or ""
431 gear.ElementalBelt.name = get_elemental_item_name("belt", weaponskill_elements) or gear.default.weaponskill_waist or ""
432end
433
434
435-- Function to get an appropriate obi/cape/ring for the current action.
436function set_elemental_obi_cape_ring(spell)
437 if spell.element == 'None' then
438 return
439 end
440
441 local world_elements = S{world.day_element}
442 if world.weather_element ~= 'None' then
443 world_elements:add(world.weather_element)
444 end
445
446 local obi_name = get_elemental_item_name("obi", S{spell.element}, world_elements)
447 gear.ElementalObi.name = obi_name or gear.default.obi_waist or ""
448
449 if obi_name then
450 if player.inventory['Twilight Cape'] or player.wardrobe['Twilight Cape'] or player.wardrobe2['Twilight Cape'] or player.wardrobe3['Twilight Cape'] or player.wardrobe4['Twilight Cape'] then
451 gear.ElementalCape.name = "Twilight Cape"
452 end
453 if (player.inventory['Zodiac Ring'] or player.wardrobe['Zodiac Ring'] or player.wardrobe2['Zodiac Ring'] or player.wardrobe3['Zodiac Ring'] or player.wardrobe4['Zodiac Ring']) then
454 if not S{'Divine Magic','Dark Magic','Healing Magic'}:contains(spell.skill) and spell.english ~= 'Impact' and spell.english ~= 'Luminohelix' and spell.english ~= 'Noctohelix' then
455 gear.ElementalRing.name = "Zodiac Ring"
456 end
457 end
458 if (player.inventory['Archon Ring'] or player.wardrobe['Archon Ring'] or player.wardrobe2['Archon Ring'] or player.wardrobe3['Archon Ring'] or player.wardrobe4['Archon Ring']) then
459 if spell.english == 'Noctohelix' then
460 gear.ElementalRing.name = "Archon Ring"
461 end
462 end
463 else
464 gear.ElementalCape.name = gear.default.obi_back
465 gear.ElementalRing.name = gear.default.obi_ring
466 end
467end
468
469
470-- Function to get the appropriate fast cast and/or recast staves for the current spell.
471function set_elemental_staff(spell)
472 if spell.action_type ~= 'Magic' then
473 return
474 end
475
476 gear.FastcastStaff.name = get_elemental_item_name("fastcast_staff", S{spell.element}) or gear.default.fastcast_staff or ""
477 gear.RecastStaff.name = get_elemental_item_name("recast_staff", S{spell.element}) or gear.default.recast_staff or ""
478end
479
480
481-- Gets the name of an elementally-aligned piece of gear within the player's
482-- inventory that matches the conditions set in the parameters.
483--
484-- item_type: Type of item as specified in the elemental_map mappings.
485-- EG: gorget, belt, obi, fastcast_staff, recast_staff
486--
487-- valid_elements: Elements that are valid for the action being taken.
488-- IE: Weaponskill skillchain properties, or spell element.
489--
490-- restricted_to_elements: Secondary elemental restriction that limits
491-- whether the item check can be considered valid.
492-- EG: Day or weather elements that have to match the spell element being queried.
493--
494-- Returns: Nil if no match was found (either due to elemental restrictions,
495-- or the gear isn't in the player inventory), or the name of the piece of
496-- gear that matches the query.
497function get_elemental_item_name(item_type, valid_elements, restricted_to_elements)
498 local potential_elements = restricted_to_elements or elements.list
499 local item_map = elements[item_type:lower()..'_of']
500
501 for element in (potential_elements.it or it)(potential_elements) do
502 if valid_elements:contains(element) and (player.inventory[item_map[element]] or player.wardrobe[item_map[element]] or player.wardrobe2[item_map[element]]) then
503 return item_map[element]
504 end
505 end
506end
507
508
509-------------------------------------------------------------------------------------------------------------------
510-- Function to easily change to a given macro set or book. Book value is optional.
511-------------------------------------------------------------------------------------------------------------------
512
513function set_macro_page(set,book)
514 if not tonumber(set) then
515 add_to_chat(123,'Error setting macro page: Set is not a valid number ('..tostring(set)..').')
516 return
517 end
518 if set < 1 or set > 10 then
519 add_to_chat(123,'Error setting macro page: Macro set ('..tostring(set)..') must be between 1 and 10.')
520 return
521 end
522
523 if book then
524 if not tonumber(book) then
525 add_to_chat(123,'Error setting macro page: book is not a valid number ('..tostring(book)..').')
526 return
527 end
528 if book < 1 or book > 20 then
529 add_to_chat(123,'Error setting macro page: Macro book ('..tostring(book)..') must be between 1 and 20.')
530 return
531 end
532 send_command('@input /macro book '..tostring(book)..';wait .1;input /macro set '..tostring(set))
533 else
534 send_command('@input /macro set '..tostring(set))
535 end
536end
537
538
539-------------------------------------------------------------------------------------------------------------------
540-- Utility functions for including local user files.
541-------------------------------------------------------------------------------------------------------------------
542
543-- Attempt to load user gear files in place of default gear sets.
544-- Return true if one exists and was loaded.
545function load_sidecar(job)
546 if not job then return false end
547
548 -- filename format example for user-local files: whm_gear.lua, or playername_whm_gear.lua
549 local filenames = {player.name..'_'..job..'_gear.lua', job..'_gear.lua',
550 'gear/'..player.name..'_'..job..'_gear.lua', 'gear/'..job..'_gear.lua',
551 'gear/'..player.name..'_'..job..'.lua', 'gear/'..job..'.lua'}
552 return optional_include(filenames)
553end
554
555-- Attempt to include user-globals. Return true if it exists and was loaded.
556function load_user_globals()
557 local filenames = {player.name..'-globals.lua', 'user-globals.lua'}
558 return optional_include(filenames)
559end
560
561-- Optional version of include(). If file does not exist, does not
562-- attempt to load, and does not throw an error.
563-- filenames takes an array of possible file names to include and checks
564-- each one.
565function optional_include(filenames)
566 for _,v in pairs(filenames) do
567 local path = gearswap.pathsearch({v})
568 if path then
569 include(v)
570 return true
571 end
572 end
573end
574
575-------------------------------------------------------------------------------------------------------------------
576-- Utility functions for vars or other data manipulation.
577-------------------------------------------------------------------------------------------------------------------
578
579-- Attempt to locate a specified name within the current alliance.
580function find_player_in_alliance(name)
581 for party_index,ally_party in ipairs(alliance) do
582 for player_index,_player in ipairs(ally_party) do
583 if _player.name == name then
584 return _player
585 end
586 end
587 end
588end
589
590
591-- buff_set is a set of buffs in a library table (any of S{}, T{} or L{}).
592-- This function checks if any of those buffs are present on the player.
593function has_any_buff_of(buff_set)
594 return buff_set:any(
595 -- Returns true if any buff from buff set that is sent to this function returns true:
596 function (b) return buffactive[b] end
597 )
598end
599
600
601-- Invert a table such that the keys are values and the values are keys.
602-- Use this to look up the index value of a given entry.
603function invert_table(t)
604 if t == nil then error('Attempting to invert table, received nil.', 2) end
605
606 local i={}
607 for k,v in pairs(t) do
608 i[v] = k
609 end
610 return i
611end
612
613
614-- Gets sub-tables based on baseSet from the string str that may be in dot form
615-- (eg: baseSet=sets, str='precast.FC', this returns the table sets.precast.FC).
616function get_expanded_set(baseSet, str)
617 local cur = baseSet
618 for i in str:gmatch("[^.]+") do
619 if cur then
620 cur = cur[i]
621 end
622 end
623
624 return cur
625end
626
627
628-------------------------------------------------------------------------------------------------------------------
629-- Utility functions data and event tracking.
630-------------------------------------------------------------------------------------------------------------------
631
632-- This is a function that can be attached to a registered event for 'time change'.
633-- It will send a call to the update() function if the time period changes.
634-- It will also call job_time_change when any of the specific time class values have changed.
635-- To activate this in your job lua, add this line to your user_setup function:
636-- windower.register_event('time change', time_change)
637--
638-- Variables it sets: classes.Daytime, and classes.DuskToDawn. They are set to true
639-- if their respective descriptors are true, or false otherwise.
640function time_change(new_time, old_time)
641 local was_daytime = classes.Daytime
642 local was_dusktime = classes.DuskToDawn
643
644 if new_time >= 6*60 and new_time < 18*60 then
645 classes.Daytime = true
646 else
647 classes.Daytime = false
648 end
649
650 if new_time >= 17*60 or new_time < 7*60 then
651 classes.DuskToDawn = true
652 else
653 classes.DuskToDawn = false
654 end
655
656 if was_daytime ~= classes.Daytime or was_dusktime ~= classes.DuskToDawn then
657 if job_time_change then
658 job_time_change(new_time, old_time)
659 end
660
661 handle_update({'auto'})
662 end
663end