local Event = require 'utils.event' local Token = require 'utils.token' local Task = require 'utils.task' local Global = require 'utils.global' local Command = require 'utils.command' local Debug = require 'utils.debug' local Gui = require 'utils.gui' local Settings = require 'utils.redmew_settings' local set_timeout_in_ticks = Task.set_timeout_in_ticks local debug_print = Debug.print local skip_btn_name = Gui.uid_name() local backward_btn_name = Gui.uid_name() local forward_btn_name = Gui.uid_name() local auto_play_cutscene_checkbox_name = Gui.uid_name() local Public = {} local handler local auto_play_cutscene_setting_name local cutscene_functions = {} local running_cutscenes = {} local replay = { identifier = nil, final_transition_time = nil } Global.register( { cutscene_functions = cutscene_functions, running_cutscenes = running_cutscenes, replay = replay }, function(tbl) cutscene_functions = tbl.cutscene_functions running_cutscenes = tbl.running_cutscenes replay = tbl.replay end ) function Public.set_auto_play_cutscene_setting_name(name) if _LIFECYCLE == _STAGE.runtime then error('Cannot set during runtime', 2) end if type(auto_play_cutscene_setting_name) == nil then error('Cannot set more than once', 2) end if type(name) ~= 'string' then error('name must be of type string', 2) end auto_play_cutscene_setting_name = name end local function valid(entity) return entity and entity.valid end local function waypoint_still_active(tick, player_index) local running_cutscene = running_cutscenes[player_index] tick = tick or -1 if tick == -1 then debug_print('Tick was nil', 5) end if not running_cutscene or tick < running_cutscene.start_tick then return false end return true end local toggle_gui_delayed = Token.register( function(params) local player = params.player if (not valid(player)) then return end if not waypoint_still_active(params.tick, player.index) then debug_print('Cutscene is no longer active. Skipping toggle_gui') return end local event = {player = player} local clear = params.clear if clear == 'left' then player.gui.left.clear() elseif clear == 'top' then player.gui.top.clear() elseif clear == 'center' then player.gui.center.clear() end params.gui.toggle(event) end ) function Public.toggle_gui(tick, player, gui, initial_delay, clear) --[[if type(gui) == 'table' then debug_print('Provided GUI is invalid.') return end]] if (not valid(player)) then return end set_timeout_in_ticks(initial_delay, toggle_gui_delayed, {tick = tick, player = player, gui = gui, clear = clear}) end local play_sound_delayed = Token.register( function(params) local player = params.player if (not valid(player)) then return end if not waypoint_still_active(params.tick, player.index) then debug_print('Cutscene is no longer active. Skipping play_sound') return end player.play_sound {path = params.path} end ) function Public.play_sound(tick, player, path, times, delay, initial_delay) if (not valid(player)) then return end if not game.is_valid_sound_path(path) then debug_print('Provided SoundPath is invalid. Try opening /radio and browse for a valid path') return end if not waypoint_still_active(tick, player.index) then debug_print('Cutscene is no longer active. Skipping play_sound') return end times = times or 1 if times == 1 and not delay and initial_delay then delay = initial_delay end if times > 1 or delay then delay = delay or 20 initial_delay = initial_delay or 0 for i = 1, times, 1 do set_timeout_in_ticks(initial_delay + delay * i, play_sound_delayed, {tick = tick, player = player, path = path}) end else player.play_sound {path = path} end end local remove_renderings = Token.register( function(renderings) for _, v in pairs(renderings) do if rendering.is_valid(v) then rendering.destroy(v) debug_print('Deleted rendering with id: ' .. v) end end end ) ---Asserts if a given variable is of the expected type using type(). --- ---@param expected_type string ---@param given any ---@param variable_reference_message string displayed when the expectation is not met local function assert_type(expected_type, given, variable_reference_message, allow_nil) local given_type = type(given) if given_type ~= expected_type and (allow_nil and given_type ~= 'nil') then error('Argument ' .. variable_reference_message .. " must be of type '" .. expected_type .. "', given '" .. given_type .. "'") end end function Public.register_cutscene_function(identifier, waypoints, func, terminate_func) assert_type('string', identifier, 'identifier of function cutscene_controller.register_cutscene_function') assert_type('table', waypoints, 'waypoints of function cutscene_controller.register_cutscene_function') assert_type('number', func, 'func of function cutscene_controller.register_cutscene_function') assert_type('number', terminate_func, 'func of function cutscene_controller.register_cutscene_function', true) cutscene_functions[identifier] = {func = func, waypoints = waypoints, update = false, terminate_func = terminate_func} end function Public.register_running_cutscene(player_index, identifier, final_transition_time) if _NO_CUTSCENE then return end assert_type('number', player_index, 'player_index of function cutscene_controller.register_running_cutscene') assert_type('string', identifier, 'identifier of function cutscene_controller.register_running_cutscene') assert_type('number', final_transition_time, 'identifier of function cutscene_controller.register_running_cutscene', true) local player = game.get_player(player_index) if not valid(player) then return end if player.controller_type ~= 1 then player.print('Cannot start cutscene without a character') return end local cutscene_function = cutscene_functions[identifier] if not cutscene_function then return end local waypoints = cutscene_function.waypoints if not waypoints then return end if running_cutscenes[player_index] then player.print({'cutscene_controller.cannot_start_new'}) return end running_cutscenes[player_index] = { func = cutscene_function.func, waypoints = waypoints, update = cutscene_function.update, final_transition_time = final_transition_time, character = player.character, terminate_func = cutscene_function.terminate_func, rendering = {}, current_index = -1, start_tick = 0 } local running_cutscene = running_cutscenes[player_index] if player.controller_type == defines.controllers.cutscene then debug_print('' .. player.name .. ' was already in another cutscene not controlled by this module. It has been stopped') player.exit_cutscene() end player.set_controller {type = defines.controllers.ghost} final_transition_time = final_transition_time >= 0 and final_transition_time or 60 running_cutscene.final_transition_time = final_transition_time running_cutscene.identifier = identifier player.set_controller { type = defines.controllers.cutscene, waypoints = waypoints, final_transition_time = final_transition_time } local flow = player.gui.top.add {type = 'flow'} running_cutscene.btn = flow local btn = flow.add {type = 'sprite-button', name = skip_btn_name, caption = 'Skip cutscene'} btn.style.minimal_height = 28 btn.style.minimal_width = 150 btn.style.font = 'default-large-bold' btn.style.font_color = {r = 255, g = 215, b = 0} local back_btn = flow.add {type = 'sprite-button', name = backward_btn_name, caption = 'Go back'} back_btn.style.minimal_height = 28 back_btn.style.minimal_width = 100 back_btn.style.font = 'default-large-bold' back_btn.style.font_color = {r = 255, g = 215, b = 0} local forward_btn = flow.add {type = 'sprite-button', name = forward_btn_name, caption = 'Go forward'} forward_btn.style.minimal_height = 28 forward_btn.style.minimal_width = 100 forward_btn.style.font = 'default-large-bold' forward_btn.style.font_color = {r = 255, g = 215, b = 0} if auto_play_cutscene_setting_name then local auto_play_cutscene_checkbox = flow.add { type = 'checkbox', name = auto_play_cutscene_checkbox_name, caption = 'Auto play cutscene', state = Settings.get(player_index, auto_play_cutscene_setting_name) } auto_play_cutscene_checkbox.style.top_margin = 8 auto_play_cutscene_checkbox.style.left_margin = 3 running_cutscene.auto_play_cutscene_checkbox = auto_play_cutscene_checkbox end handler({player_index = player_index, waypoint_index = -1, tick = game.tick}) end local function restart_cutscene(player_index, waypoints, start_index) local current_running = running_cutscenes[player_index] local final_transition_time = current_running.final_transition_time current_running.update = false local character = current_running.character if not character then log('Player index: ' .. player_index .. ' managed to lose their character in a cutscene') end local end_waypoint = { -- end waypoint position = character.position, transition_time = final_transition_time, time_to_wait = 1, zoom = 1, terminate = true } table.insert(waypoints, end_waypoint) running_cutscenes[player_index] = { func = current_running.func, waypoints = waypoints, update = false, final_transition_time = final_transition_time, character = character, terminate_func = current_running.terminate_func, rendering = current_running.rendering, btn = current_running.btn, current_index = current_running.current_index, start_tick = current_running.start_tick } debug_print('Updating cutscene for player_index ' .. player_index) debug_print(running_cutscenes[player_index]) local player = game.get_player(player_index) if not valid(player) then return end if player.controller_type == defines.controllers.cutscene then player.exit_cutscene() player.set_controller {type = defines.controllers.ghost} end player.set_controller { type = defines.controllers.cutscene, waypoints = waypoints, final_transition_time = final_transition_time } if start_index then player.jump_to_cutscene_waypoint(start_index + 1) else start_index = -1 end handler({player_index = player_index, waypoint_index = start_index, tick = game.tick}) end function Public.inject_waypoint(player_index, waypoint, waypoint_index, override) local running_cutscene = running_cutscenes[player_index] if not running_cutscene then return end local waypoints = running_cutscene.waypoints if not waypoints then return end local copy_waypoints = {} for i = 1, #waypoints do table.insert(copy_waypoints, waypoints[i]) end if override then copy_waypoints[waypoint_index] = waypoint else table.insert(copy_waypoints, waypoint_index, waypoint) end running_cutscene.update = copy_waypoints end local callback_function = Token.register( function(params) local player_index = params.player_index local func_params = params.params if waypoint_still_active(func_params.tick, player_index) then Token.get(params.func)(player_index, params.waypoint_index, func_params) else debug_print('Skipping callback function. Cutscene got terminated!') end end ) local reconnect_character = Token.register( function(params) local player_index = params.player_index local player = game.get_player(player_index) local running_cutscene = params.running_cutscene local character = running_cutscene.character local func = running_cutscene.terminate_func if valid(player) then player.exit_cutscene() if valid(character) then player.set_controller {type = defines.controllers.character, character = character} else params.skip_btn_flag = true player.ticks_to_respawn = 3600 player.print('[color=red][font=compi]Oops - You Died! Please go to a safe location and restart the cutscene[/font][/color]') end if func then Token.get(func)(player_index, params.skip_btn_flag) end Token.get(remove_renderings)(running_cutscene.rendering) running_cutscene.btn.destroy() running_cutscenes[player_index] = nil end end ) function Public.terminate_cutscene(player_index, ticks,skip_btn_flag) local running_cutscene = running_cutscenes[player_index] if not running_cutscene then return end ticks = ticks and ticks or 1 debug_print('Terminating cutscene in ' .. ticks .. ' Ticks') if skip_btn_flag == nil then skip_btn_flag = false end debug_print('Is terminate_func ignored = ' .. tostring(skip_btn_flag)) set_timeout_in_ticks( ticks, reconnect_character, { player_index = player_index, running_cutscene = running_cutscene, skip_btn_flag = skip_btn_flag } ) end function Public.register_rendering_id(player_index, tick, render_id) if type(render_id) ~= 'table' then render_id = {render_id} end local running_cutscene = running_cutscenes[player_index] for _, id in pairs(render_id) do if rendering.is_valid(id) then if not waypoint_still_active(tick, player_index) then debug_print('The rendering with id ' .. id .. ' was not added. Destroying it instead') rendering.destroy(id) else table.insert(running_cutscene.rendering, id) end end end end function Public.register_replay(identifier, final_transition_time) replay.identifier = identifier replay.final_transition_time = final_transition_time debug_print('Identifier ' .. identifier .. ' registered as replay cutscene') end handler = function(event) local player_index = event.player_index local waypoint_index = event.waypoint_index local tick = event.tick debug_print('Waypoint_index ' .. waypoint_index .. ' has finished at tick: ' .. tick) local running_cutscene = running_cutscenes[player_index] if not running_cutscene then return end running_cutscene.current_index = waypoint_index + 1 running_cutscene.start_tick = tick local update = running_cutscene.update if update then restart_cutscene(player_index, update, waypoint_index) return end local ticks = running_cutscene.waypoints[waypoint_index + 2] if ticks then ticks = ticks.transition_time else ticks = running_cutscene.final_transition_time end local func = running_cutscene.func if not func then return end local current_waypoint = running_cutscene.waypoints[waypoint_index + 2] if not current_waypoint or current_waypoint.terminate then Public.terminate_cutscene(player_index, ticks) return end local params = { position = current_waypoint.position, time_to_wait = current_waypoint.time_to_wait, transition_time = current_waypoint.transition_time, zoom = current_waypoint.zoom, name = current_waypoint.name, tick = tick } debug_print('Waypoint_index ' .. waypoint_index + 1 .. ' (waypoint #' .. waypoint_index + 2 .. ') callback in ' .. ticks .. ' ticks') set_timeout_in_ticks(ticks, callback_function, {func = running_cutscene.func, player_index = player_index, waypoint_index = waypoint_index, params = params}) end function Public.goTo(player_index, waypoint_index) local running_cutscene = running_cutscenes[player_index] if waypoint_index < 0 or waypoint_index > #running_cutscene.waypoints - 2 then return false end Token.get(remove_renderings)(running_cutscene.rendering) game.get_player(player_index).jump_to_cutscene_waypoint(waypoint_index) handler({player_index = player_index, waypoint_index = waypoint_index - 1, tick = game.tick}) running_cutscene.current_index = waypoint_index return true end local function restore(event) Public.terminate_cutscene(event.player_index, 1, true) end Event.add(defines.events.on_cutscene_waypoint_reached, handler) Event.add(defines.events.on_pre_player_left_game, restore) Event.add(defines.events.on_player_joined_game, restore) local replay_cutscene = Token.register( function(params) Public.register_running_cutscene(params.event.player_index, replay.identifier, replay.final_transition_time) end ) local function replay_handler(_, player) if not replay.identifier then player.print({'cutscene_controller.cannot_replay'}) return end Token.get(replay_cutscene)({event = {player_index = player.index}}) end Command.add( 'replay', { description = {'cutscene_controller.replay'}, capture_excess_arguments = false, allowed_by_server = false }, replay_handler ) local function skip_cutscene(_, player) if not player or not player.valid then return end if player.controller_type == defines.controllers.cutscene then Public.terminate_cutscene(player.index, 1, true) end end Command.add( 'skip', { description = {'cutscene_controller.skip'}, capture_excess_arguments = false, allowed_by_server = false }, skip_cutscene ) Gui.on_click( skip_btn_name, function(event) skip_cutscene(nil, game.get_player(event.player_index)) end ) Gui.on_click( backward_btn_name, function(event) local player_index = event.player_index if Public.goTo(player_index, running_cutscenes[player_index].current_index - 1) == false then game.get_player(player_index).print("Cutscene: You're already at the beginning") end end ) Gui.on_click( forward_btn_name, function(event) local player_index = event.player_index if Public.goTo(event.player_index, running_cutscenes[player_index].current_index + 1) == false then game.get_player(player_index).print("Cutscene: You're already at the end") end end ) Gui.on_checked_state_changed(auto_play_cutscene_checkbox_name, function(event) local checkbox = event.element Settings.set(event.player_index, auto_play_cutscene_setting_name, checkbox.state) end) Event.add(Settings.events.on_setting_set, function(event) if auto_play_cutscene_setting_name == nil or event.setting_name ~= auto_play_cutscene_setting_name then return end local running_cutscene = running_cutscenes[event.player_index] if not running_cutscene then return end running_cutscene.auto_play_cutscene_checkbox.state = event.new_value end) return Public