Merge pull request #2735 from tobylane/mapoverlay

Add level file overlays
This commit is contained in:
Stephen E. Baker
2025-06-09 12:40:32 -04:00
committed by GitHub
4 changed files with 108 additions and 43 deletions

View File

@@ -635,15 +635,15 @@ function App:loadCampaign(campaign_file)
campaign_info.winning_text_table)
if self:loadLevel(level_info.path, nil, level_info.name,
level_info.map_file, level_info.briefing, nil, _S.errors.load_level_prefix, campaign_info) then
level_info.map_file, level_info.briefing, nil, _S.errors.load_level_prefix, campaign_info) and self.world then
-- The new world needs to know which campaign to continue on.
self.world.campaign_info = campaign_info
end
-- Play the level advance movie from a position where this campaign will end at 12
if campaign_info.movie then
local n = math.max(1, 12 - #campaign_info.levels)
self.moviePlayer:playAdvanceMovie(n)
-- Play the level advance movie from a position where this campaign will end at 12
if campaign_info.movie then
local n = math.max(1, 12 - #campaign_info.levels)
self.moviePlayer:playAdvanceMovie(n)
end
end
end
@@ -769,7 +769,7 @@ function App:_loadLevel(level, difficulty, level_name, level_file, level_intro,
local new_map = Map(self)
local map_objects, errors = new_map:load(level, difficulty, level_name, level_file, level_intro, map_editor)
if not map_objects then
self.world.ui:addWindow(UIInformation(self.ui, { errors }))
self.ui:addWindow(UIInformation(self.ui, { errors }))
return
end
-- If going from another level, save progress.

View File

@@ -197,7 +197,7 @@ end
function UINewGame:startGame(difficulty)
if self.ui.app:loadLevel(1, difficulty, nil, nil, nil, nil,
_S.errors.load_level_prefix, nil) then
_S.errors.load_level_prefix, nil) and self.ui.app.world then
self.ui.app.moviePlayer:playAdvanceMovie(1)
-- Initiate campaign progression. The UI above may now have changed.

View File

@@ -774,7 +774,14 @@ errors = {
full_in_demo = "Sorry, you can't open a full game save with the demo files loaded. Please update your TH Data folder setting.",
},
music = "There are playback issues with one or more files in your music directory. Problematic files will be disabled in the jukebox. See the console window for more information.",
missing_corsixth_file = "Warning: Could not find file %s, try reinstalling CorsixTH.",
missing_th_data_file = "Warning: Could not find file %s, the Theme Hospital data is incomplete.",
missing_level_file = "Error: Could not find the chosen level file.",
overlay = {
incorrect_difficulty = "Overlay difficulty must be easy, full or hard. Current value is ",
incorrect_level_number = "Overlay level number must be 1-12. Current value is ",
missing_setting = "No difficulty and level number given in overlay field of custom level.",
},
cannot_restart_missing_files = "Sorry, but this level cannot be restarted because of missing files %s or %s.",
}

View File

@@ -167,32 +167,26 @@ the original game levels are considered.
!param map_file (string) The path to the map file as supplied by the config file.
!param level_intro (string) If loading a custom level this message will be shown as soon as the level
has been loaded.
!return objects (table) The loaded map objects if successfully loaded
!return errors (string) A localised error message if objects were not successfully loaded
]]
function Map:load(level, difficulty, level_name, map_file, level_intro, map_editor)
local objects, _
local objects
if not difficulty then
difficulty = "full"
end
-- Load CorsixTH base configuration for all levels.
-- We want to load the file again each time.
local function file (filename)
local f = assert(loadfile(filename))
return f()
end
local result = file(self.app:getFullPath({"Lua", "base_config.lua"}))
local base_config = result
-- Load CorsixTH base configuration for all levels.
local base_config = loadfile(self.app:getFullPath({"Lua", "base_config.lua"}))
if not base_config then
return nil, _S.errors.missing_corsixth_file:format("base_config.lua")
end
base_config = base_config()
assert(base_config, "No base config has been loaded!")
if type(level) == "number" then
local errors, data
-- Playing the original campaign.
-- Add TH's base config if possible, otherwise our own config
-- roughly corresponds to "full".
errors, base_config = self:loadMapConfig(difficulty .. "00.SAM", base_config)
-- If it couldn't be loaded the new difficulty level is full no matter what.
if errors then
difficulty = "full"
end
self.difficulty = difficulty
local errors, data, _, result
self.level_number = level
data, errors = self:getRawData(map_file)
if data then
@@ -207,22 +201,19 @@ function Map:load(level, difficulty, level_name, map_file, level_intro, map_edit
local demo_path = self.app:getFullPath({"Levels", "demo.level"})
errors, result = self:loadMapConfig(demo_path, base_config, true)
if errors then
print("Warning: Could not find the demo configuration, try reinstalling the game")
return nil, _S.errors.missing_corsixth_file:format("demo.level")
end
self.difficulty = "full"
self.level_config = result
else
local level_no = level
if level_no < 10 then
level_no = "0" .. level
errors, result = self:_loadOriginalCampaignLevel(difficulty, level, base_config)
if errors then
return nil, errors
end
-- Override with the specific configuration for this level
_, result = self:loadMapConfig(difficulty .. level_no .. ".SAM", base_config)
-- Finally load additional CorsixTH config per level
local level_path = self.app:getFullPath({"Levels", "original" .. level_no .. ".level"})
_, result = self:loadMapConfig(level_path, result, true)
self.level_config = result
end
elseif map_editor then
local _
-- We're being fed data by the map editor.
self.level_name = "MAP EDITOR"
self.level_number = "MAP EDITOR"
@@ -236,10 +227,9 @@ function Map:load(level, difficulty, level_name, map_file, level_intro, map_edit
return nil, errors
end
end
assert(base_config, "No base config has been loaded!")
self.level_config = base_config
else
local _, result
-- We're loading a custom level.
self.level_name = level_name
self.level_intro = level_intro
@@ -252,10 +242,9 @@ function Map:load(level, difficulty, level_name, map_file, level_intro, map_edit
else
return nil, errors
end
assert(base_config, "No base config has been loaded!")
errors, result = self:loadMapConfig(level, base_config, true)
if errors then
print(errors)
return nil, errors
end
self.level_config = result
end
@@ -279,6 +268,46 @@ function Map:load(level, difficulty, level_name, map_file, level_intro, map_edit
return objects
end
-- A set of the TH campaign levels with additional CorsixTH config files, found in CorsixTH/Levels.
local additional_config = list_to_set({"05", "07", "08", 11, 12})
--! Load further level configurations for the main campaign.
--!param difficulty (string)
--!param level_number (integer)
--!param config (table) The level config created so far
--!return error (string) Localised error message if error happens
--!return config (table) Level config, if loaded successfully
function Map:_loadOriginalCampaignLevel(difficulty, level_number, config)
local errors
if level_number < 10 then
level_number = "0" .. level_number
end
-- Add TH's base config if possible, otherwise our own config,
-- which roughly corresponds to "full".
local filename_diff = difficulty .. "00.SAM"
errors, config = self:loadMapConfig(filename_diff, config)
if errors then
return _S.errors.missing_th_data_file:format(filename_diff)
end
self.difficulty = difficulty
-- Load the Theme Hospital configuration for this difficulty and level
local filename_th_data = difficulty .. level_number .. ".SAM"
errors, config = self:loadMapConfig(filename_th_data, config)
if errors then
return _S.errors.missing_th_data_file:format(filename_th_data)
end
if additional_config[level_number] then
-- Load additional CorsixTH config per level.
local filename_cth = "original" .. level_number .. ".level"
local level_path = self.app:getFullPath({"Levels", filename_cth})
errors, config = self:loadMapConfig(level_path, config, true)
if errors then
return _S.errors.missing_corsixth_file:format(filename_cth)
end
end
return nil, config
end
--! Get the difficulty of the level. Custom levels and campaign always have medium difficulty.
--!return (int) difficulty of the level, 1=easy, 2=medium, 3=hard.
function Map:getDifficulty()
@@ -352,13 +381,17 @@ function Map:save(filename)
self.th:save(filename)
end
--[[! Loads map configurations from files. Returns nil as first result
if no configuration could be loaded and config as second result no matter what.
-- A set of the level difficulties. Full is default
local correct_difficulties = list_to_set({"easy", "full", "hard"})
--[[! Loads map configurations from files.
!param filename (string) The absolute path to the config file to load
!param config (string) If a base config already exists and only some values should be overridden
this is the base config
!param custom If true The configuration file is searched for where filename points, otherwise
it is assumed that we're looking in the theme_hospital_install path.
!return error The specific localised error message, if error occurred
!return config The successfully loaded config
]]
function Map:loadMapConfig(filename, config, custom)
local function iterator()
@@ -368,7 +401,7 @@ function Map:loadMapConfig(filename, config, custom)
return self.app.fs:readContents("Levels", filename):gmatch"[^\r\n]+"
end
end
if self.app.fs:readContents("Levels", filename) or io.open(filename) then
if filename and (self.app.fs:readContents("Levels", filename) or io.open(filename)) then
for line in iterator() do
if line:sub(1, 1) == "#" then
local parts = {}
@@ -403,9 +436,34 @@ function Map:loadMapConfig(filename, config, custom)
end
end
end
-- If the level file overlay field gives a difficulty and number (from the TH campaign),
-- try to load that on top of the config loaded so far.
if config.overlay then
local difficulty, level_number = config.overlay.difficulty, config.overlay.level_number
local errors
config.overlay = nil -- Prevent recursive loop
if difficulty and level_number then
-- Validate the difficulty and level number
if not correct_difficulties[difficulty] then
return _S.errors.overlay.incorrect_difficulty .. difficulty
end
if type(level_number) ~= "number" or level_number < 1 or level_number > 12 then
return _S.errors.overlay.incorrect_level_number .. level_number
end
-- Load difficulty and level from Theme Hospital and CorsixTH configs
errors, config = self:_loadOriginalCampaignLevel(difficulty, level_number, config)
if errors then
return errors
end
else
return _S.errors.overlay.missing_setting
end
end
return nil, config
else
return "Error: Could not find the configuration file, only 'Base Config' will be loaded for this level.", config
return _S.errors.missing_level_file
end
end