1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2024-12-14 10:13:13 +02:00
RedMew/features/cutscene/cutscene_controller.lua
2021-03-31 12:01:17 +01:00

613 lines
20 KiB
Lua

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