Adds original movies from Theme Hospital to CorsixTH. See issue 1428 for more information.

This commit is contained in:
Stephen E. Baker
2013-01-11 03:17:58 +00:00
parent a37fa21d4b
commit 22f302c115
25 changed files with 1804 additions and 17 deletions

6
.gitattributes vendored
View File

@@ -1,7 +1,9 @@
* text=auto !eol
AnimView/AnimView.ico -text
AnimView/AnimView.rc -text
CMake/COPYING-CMAKE-SCRIPTS -text
CMake/FindDirectX.cmake -text
CMake/FindFFmpeg.cmake -text
CMake/FindPkgMacros.cmake -text
CorsixTH/Bitmap/aux_ui.dat -text svneol=unset#unset
CorsixTH/Bitmap/aux_ui.spec -text
@@ -64,8 +66,12 @@ CorsixTH/Levels/original07.level -text
CorsixTH/Levels/original11.level -text
CorsixTH/Levels/original12.level -text
CorsixTH/Lua/dialogs/fullscreen/graphs.lua -text
CorsixTH/Lua/movie_player.lua -text
CorsixTH/Original_Logo.svg -text
CorsixTH/Original_Logo_Text.svg -text
CorsixTH/Src/th_lua_movie.cpp -text
CorsixTH/Src/th_movie.cpp -text
CorsixTH/Src/th_movie.h -text
CorsixTH/__MACOSX/SDLMain/._SDLMain.m -text
DebianPackage/control -text
DebianPackage/debian-binary -text

View File

@@ -0,0 +1,22 @@
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

150
CMake/FindFFmpeg.cmake Normal file
View File

@@ -0,0 +1,150 @@
# vim: ts=2 sw=2
# - Try to find the required ffmpeg components(default: AVFORMAT, AVUTIL, AVCODEC)
#
# Once done this will define
# FFMPEG_FOUND - System has the all required components.
# FFMPEG_INCLUDE_DIRS - Include directory necessary for using the required components headers.
# FFMPEG_LIBRARIES - Link these to use the required ffmpeg components.
# FFMPEG_DEFINITIONS - Compiler switches required for using the required ffmpeg components.
#
# For each of the components it will additionally set.
# - AVCODEC
# - AVDEVICE
# - AVFORMAT
# - AVUTIL
# - POSTPROCESS
# - SWSCALE
# - SWRESAMPLE
# the following variables will be defined
# <component>_FOUND - System has <component>
# <component>_INCLUDE_DIRS - Include directory necessary for using the <component> headers
# <component>_LIBRARIES - Link these to use <component>
# <component>_DEFINITIONS - Compiler switches required for using <component>
# <component>_VERSION - The components version
#
# Copyright (c) 2006, Matthias Kretz, <kretz@kde.org>
# Copyright (c) 2008, Alexander Neundorf, <neundorf@kde.org>
# Copyright (c) 2011, Michael Jansen, <kde@michael-jansen.biz>
#
# Redistribution and use is allowed according to the terms of the BSD license.
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
include(FindPackageHandleStandardArgs)
# The default components were taken from a survey over other FindFFMPEG.cmake files
if (NOT FFmpeg_FIND_COMPONENTS)
set(FFmpeg_FIND_COMPONENTS AVCODEC AVFORMAT AVUTIL)
endif ()
#
### Macro: set_component_found
#
# Marks the given component as found if both *_LIBRARIES AND *_INCLUDE_DIRS is present.
#
macro(set_component_found _component )
if (${_component}_LIBRARIES AND ${_component}_INCLUDE_DIRS)
# message(STATUS " - ${_component} found.")
set(${_component}_FOUND TRUE)
else ()
# message(STATUS " - ${_component} not found.")
endif ()
endmacro()
#
### Macro: find_component
#
# Checks for the given component by invoking pkgconfig and then looking up the libraries and
# include directories.
#
macro(find_component _component _pkgconfig _library _header)
if (NOT WIN32)
# use pkg-config to get the directories and then use these values
# in the FIND_PATH() and FIND_LIBRARY() calls
find_package(PkgConfig)
if (PKG_CONFIG_FOUND)
pkg_check_modules(PC_${_component} ${_pkgconfig})
endif ()
endif (NOT WIN32)
find_path(${_component}_INCLUDE_DIRS ${_header}
HINTS
${PC_LIB${_component}_INCLUDEDIR}
${PC_LIB${_component}_INCLUDE_DIRS}
PATH_SUFFIXES
ffmpeg
)
find_library(${_component}_LIBRARIES NAMES ${_library}
HINTS
${PC_LIB${_component}_LIBDIR}
${PC_LIB${_component}_LIBRARY_DIRS}
)
set(${_component}_DEFINITIONS ${PC_${_component}_CFLAGS_OTHER} CACHE STRING "The ${_component} CFLAGS.")
set(${_component}_VERSION ${PC_${_component}_VERSION} CACHE STRING "The ${_component} version number.")
set_component_found(${_component})
mark_as_advanced(
${_component}_INCLUDE_DIRS
${_component}_LIBRARIES
${_component}_DEFINITIONS
${_component}_VERSION)
endmacro()
# Check for cached results. If there are skip the costly part.
if (NOT FFMPEG_LIBRARIES)
# Check for all possible component.
find_component(AVCODEC libavcodec avcodec libavcodec/avcodec.h)
find_component(AVFORMAT libavformat avformat libavformat/avformat.h)
find_component(AVDEVICE libavdevice avdevice libavdevice/avdevice.h)
find_component(AVUTIL libavutil avutil libavutil/avutil.h)
find_component(SWSCALE libswscale swscale libswscale/swscale.h)
find_component(POSTPROC libpostproc postproc libpostproc/postprocess.h)
find_component(SWRESAMPLE libswresample swresample libswresample/swresample.h)
# Check if the required components were found and add their stuff to the FFMPEG_* vars.
foreach (_component ${FFmpeg_FIND_COMPONENTS})
if (${_component}_FOUND)
# message(STATUS "Required component ${_component} present.")
set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} ${${_component}_LIBRARIES})
set(FFMPEG_DEFINITIONS ${FFMPEG_DEFINITIONS} ${${_component}_DEFINITIONS})
list(APPEND FFMPEG_INCLUDE_DIRS ${${_component}_INCLUDE_DIRS})
else ()
# message(STATUS "Required component ${_component} missing.")
endif ()
endforeach ()
# Build the include path with duplicates removed.
if (FFMPEG_INCLUDE_DIRS)
list(REMOVE_DUPLICATES FFMPEG_INCLUDE_DIRS)
endif ()
# cache the vars.
set(FFMPEG_INCLUDE_DIRS ${FFMPEG_INCLUDE_DIRS} CACHE STRING "The FFmpeg include directories." FORCE)
set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} CACHE STRING "The FFmpeg libraries." FORCE)
set(FFMPEG_DEFINITIONS ${FFMPEG_DEFINITIONS} CACHE STRING "The FFmpeg cflags." FORCE)
mark_as_advanced(FFMPEG_INCLUDE_DIRS
FFMPEG_LIBRARIES
FFMPEG_DEFINITIONS)
endif ()
# Now set the noncached _FOUND vars for the components.
foreach (_component AVCODEC AVDEVICE AVFORMAT AVUTIL POSTPROCESS SWSCALE SWRESAMPLE)
set_component_found(${_component})
endforeach ()
# Compile the list of required vars
set(_FFmpeg_REQUIRED_VARS FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS)
foreach (_component ${FFmpeg_FIND_COMPONENTS})
list(APPEND _FFmpeg_REQUIRED_VARS ${_component}_LIBRARIES ${_component}_INCLUDE_DIRS)
endforeach ()
# Give a nice error message if some of the required vars are missing.
find_package_handle_standard_args(FFmpeg DEFAULT_MSG ${_FFmpeg_REQUIRED_VARS})

View File

@@ -12,6 +12,7 @@
PROJECT(CorsixTH_Top_Level)
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
SET(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/CMake)
INCLUDE(CheckIncludeFiles)
SET(CORSIX_TH_DONE_TOP_LEVEL_CMAKE ON)
@@ -20,6 +21,7 @@ OPTION(WITH_OPENGL "Activate OpenGL Renderer" OFF)
OPTION(WITH_DIRECTX "Activate DirectX Renderer" OFF)
OPTION(WITH_SDL "Activate SDL Renderer" ON) # our default option
OPTION(WITH_AUDIO "Activate Sound" ON) # enabled by default
OPTION(WITH_MOVIES "Activate in game movies" ON)
OPTION(WITH_FREETYPE2 "Enhanced Font Support" ON)
OPTION(BUILD_ANIMVIEWER "Build the animation viewer as part of the build process" OFF)
OPTION(BUILD_MAPEDITOR "Build the map editor as part of the build process" OFF)
@@ -66,6 +68,14 @@ ELSE()
MESSAGE("Note: SDL audio is disabled")
ENDIF(WITH_AUDIO)
IF(WITH_MOVIES)
SET(CORSIX_TH_USE_FFMPEG ON)
MESSAGE("Note: FFMPEG video is enabled (default)")
ELSE()
SET(CORSIX_TH_USE_FFMPEG OFF)
MESSAGE("Note: FFMPEG video is disabled")
ENDIF(WITH_MOVIES)
IF(WITH_FREETYPE2)
SET(CORSIX_TH_USE_FREETYPE2 ON)
MESSAGE("Note: FreeType2 is enabled (default)")

View File

@@ -137,6 +137,20 @@ IF(CORSIX_TH_USE_SDL_MIXER)
ENDIF(SDLMIXER_FOUND)
ENDIF(CORSIX_TH_USE_SDL_MIXER)
message( STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}" )
# Find FFMPEG
IF(CORSIX_TH_USE_FFMPEG)
FIND_PACKAGE(FFmpeg COMPONENTS AVFORMAT AVCODEC AVUTIL SWSCALE SWRESAMPLE REQUIRED)
IF(FFMPEG_FOUND)
TARGET_LINK_LIBRARIES(CorsixTH ${FFMPEG_LIBRARIES})
INCLUDE_DIRECTORIES(${FFMPEG_INCLUDE_DIRS})
message(" FFmpeg found")
ELSE(FFMPEG_FOUND)
message("Error: FFmpeg library not found, even though it was selected to be included")
ENDIF(FFMPEG_FOUND)
ENDIF(CORSIX_TH_USE_FFMPEG)
# Find Freetype2
IF(CORSIX_TH_USE_FREETYPE2)
FIND_PACKAGE(Freetype REQUIRED)
@@ -163,7 +177,6 @@ ENDIF(CORSIX_TH_USE_OGL_RENDERER)
# Find DirectX
IF(CORSIX_TH_USE_DX9_RENDERER)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/CMake")
FIND_PACKAGE(DirectX REQUIRED)
IF(DirectX_FOUND)
TARGET_LINK_LIBRARIES(CorsixTH ${DirectX_LIBRARY})
@@ -174,6 +187,15 @@ IF(CORSIX_TH_USE_DX9_RENDERER)
message(FATAL_ERROR "Error: DirectX library not found, it is required to build (consider changing choice of renderer)")
ENDIF(DirectX_FOUND)
ENDIF(CORSIX_TH_USE_DX9_RENDERER)
# Find msinttypes for MSVC
IF(MSVC)
FIND_PATH(MSINTTYPES_INCLUDE_DIRS "inttypes.h" NO_DEFAULT_PATH)
MARK_AS_ADVANCED(MSINTTYPES_INCLUDE_DIRS)
MESSAGE(STATUS "Adding include directory: ${MSINTTYPES_INCLUDE_DIRS}")
INCLUDE_DIRECTORIES(${MSINTTYPES_INCLUDE_DIRS})
ENDIF(MSVC)
# Declaration of the install process
IF(APPLE)
#Just use the prefix as it's sufficient to just set the prefix to /Applications on Mac.

View File

@@ -49,6 +49,8 @@ function App:App()
motion = self.onMouseMove,
active = self.onWindowActive,
music_over = self.onMusicOver,
movie_allocate_picture = self.onMovieAllocatePicture,
movie_over = self.onMovieOver
}
self.strings = {}
self.savegame_version = SAVEGAME_VERSION
@@ -203,6 +205,13 @@ function App:init()
self.audio = Audio(self)
self.audio:init()
-- Load movie player
dofile "movie_player"
self.moviePlayer = MoviePlayer(self, self.audio)
if good_install_folder then
self.moviePlayer:init()
end
-- Load strings before UI and before additional Lua
dofile "strings"
dofile "string_extensions"
@@ -265,6 +274,9 @@ function App:init()
self:loadLevel("")
else
self:loadMainMenu()
if self.config.play_intro then
self.moviePlayer:playIntro()
end
-- If we couldn't properly load the language, show an information dialog
if not language_load_success then
-- At this point we know the language is english, so no use having
@@ -400,7 +412,11 @@ function App:loadLevel(level, ...)
-- Load UI
self.ui = GameUI(self, self.world:getLocalPlayerHospital())
self.world:setUI(self.ui) -- Function call allows world to set up its keyHandlers
if tonumber(level) then
self.moviePlayer:playAdvanceMovie(level)
end
-- Now restore progress from previous levels.
if carry_to_next_level then
self.world:initFromPreviousLevel(carry_to_next_level)
@@ -749,10 +765,12 @@ function App:dispatch(evt_type, ...)
end
function App:onTick(...)
if self.world then
self.world:onTick(...)
if(not self.moviePlayer.playing) then
if self.world then
self.world:onTick(...)
end
self.ui:onTick(...)
end
self.ui:onTick(...)
return true -- tick events always result in a repaint
end
@@ -762,9 +780,13 @@ local fps_sum = 0 -- Sum of fps_history array
local fps_next = 1 -- Used to loop through fps_history when [over]writing
function App:drawFrame()
self.video:startFrame()
self.ui:draw(self.video)
self.video:endFrame()
if(self.moviePlayer.playing) then
self.moviePlayer:refresh()
else
self.video:startFrame()
self.ui:draw(self.video)
self.video:endFrame()
end
if self.config.track_fps then
fps_sum = fps_sum - fps_history[fps_next]
@@ -808,6 +830,14 @@ function App:onMusicOver(...)
return self.audio:onMusicOver(...)
end
function App:onMovieAllocatePicture(...)
return self.moviePlayer:onMovieAllocatePicture(...)
end
function App:onMovieOver(...)
self.moviePlayer:onMovieOver(...)
end
function App:checkInstallFolder()
self.fs = FileSystem()
local status, err

View File

@@ -36,6 +36,7 @@ function Audio:Audio(app)
}
self.has_bg_music = false
self.not_loaded = not app.config.audio
self.abort_bg_music = false
end
local function GetFileData(path)
@@ -383,6 +384,16 @@ function Audio:pauseBackgroundTrack()
self:notifyJukebox()
end
function Audio:stopBackgroundMusic()
self:stopBackgroundTrack()
self.abort_bg_music = true
end
function Audio:resumeBackgroundMusic()
self.abort_bg_music = false
self:playRandomBackgroundTrack()
end
function Audio:stopBackgroundTrack()
if self.background_paused then
-- unpause first in order to clear the backupped volume
@@ -431,12 +442,14 @@ function Audio:playBackgroundTrack(index)
end)
return
end
SDL.audio.setMusicVolume(self.app.config.music_volume)
assert(SDL.audio.playMusic(music))
self.background_music = music
-- Update configuration that we want music
self.app.config.play_music = not not self.background_music
self:notifyJukebox()
if not self.abort_bg_music then
SDL.audio.setMusicVolume(self.app.config.music_volume)
assert(SDL.audio.playMusic(music))
self.background_music = music
-- Update configuration that we want music
self.app.config.play_music = not not self.background_music
self:notifyJukebox()
end
end
function Audio:onMusicOver()
@@ -483,3 +496,17 @@ function Audio:notifyJukebox()
jukebox:updatePlayButton()
end
end
function Audio:reserveChannel()
if self.sound_fx then
return self.sound_fx:reserveChannel()
else
return -1
end
end
function Audio:releaseChannel(channel)
if self.sound_fx and channel > -1 then
self.sound_fx:releaseChannel(channel)
end
end

View File

@@ -105,6 +105,8 @@ local config_defaults = {
adviser_disabled = false,
allow_user_actions_while_paused = false,
warmth_colors_display_default = 1,
movies = true,
play_intro = true
}
local fi = io.open(config_filename, "r")
local config_values = {}
@@ -176,6 +178,9 @@ sound_volume = ]=].. tostring(config_values.sound_volume) ..[=[
-- Adviser on/off: If you set this setting to true the adviser will no longer
-- pop up.
adviser_disabled = ]=].. tostring(config_values.adviser_disabled) ..[=[
--
-- Intro movie: By default enabled
play_intro = ]=].. tostring(config_values.play_intro) ..[=[
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
@@ -292,6 +297,13 @@ warmth_colors_display_default = ]=].. tostring(config_values.warmth_colors_displ
-- present in the original game. Examples include Russian, Chinese and Polish.
-- unicode_font = [[X:\ThemeHospital\font.ttc]]
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- Movie global on/off switch.
-- Note that movies will also be disabled if CorsixTH was compiled without the
-- FFMPEG library.
movies = ]=].. tostring(config_values.movies) ..[=[
-------------------------------------------------------------------------------
]=])
end
end

View File

@@ -168,6 +168,7 @@ function UIFax:choice(choice)
-- TODO: Allow some kind of custom campaign with custom levels
end
elseif choice == "return_to_main_menu" then
self.ui.app.moviePlayer:playWinMovie()
self.ui.app:loadMainMenu()
end
self.icon:removeMessage()

View File

@@ -203,7 +203,13 @@ function GameUI:onKeyDown(code, rawchar)
end
rawchar = self.key_code_to_rawchar[code] -- UI may have translated rawchar
local key = self:_translateKeyCode(code, rawchar)
--abort movies
if self.app.moviePlayer.playing then
if key == "esc" or key == " " then
self.app.moviePlayer:stop()
end
return true
end
--Maybe the player wants to abort an "about to edit room" action
if key == "esc" and self.edit_room then
self:setEditRoom(false)
@@ -381,6 +387,9 @@ end
-- TODO: try to remove duplication with UI:onMouseMove
function GameUI:onMouseMove(x, y, dx, dy)
local repaint = UpdateCursorPosition(self.app.video, x, y)
if self.app.moviePlayer.playing then
return false
end
self.cursor_x = x
self.cursor_y = y
@@ -460,6 +469,10 @@ function GameUI:onMouseMove(x, y, dx, dy)
end
function GameUI:onMouseUp(code, x, y)
if self.app.moviePlayer.playing then
return UI.onMouseUp(self, code, x, y)
end
if code == 4 or code == 5 then
-- Mouse wheel
local window = self:getWindow(UIFullscreen)

View File

@@ -0,0 +1,183 @@
--[[ Copyright (c) 2012 Stephen Baker
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. --]]
--! Layer which handles the Lua-facing side of loading and playing video.
local TH = require "TH"
local pathsep = package.config:sub(1, 1)
class "MoviePlayer"
function MoviePlayer:MoviePlayer(app, audio)
self.app = app
self.audio = audio
self.playing = false
self.can_skip = true
self.holding_bg_music = false
self.channel = -1
self.lose_movies = {}
self.advance_movies = {}
self.intro_movie = nil
self.win_movie = nil
end
function MoviePlayer:init()
self.moviePlayer = TH.moviePlayer()
--find movies in Anims folder
local num
local movie
local movies = self.app.fs:listFiles("Anims");
if movies then
for _,movie in pairs(movies) do
--lose level movies
if movie:upper():match(pathsep .. "LOSE%d+%.[^" .. pathsep .."]+$") then
table.insert(self.lose_movies, movie)
end
--advance level movies
num = tonumber(movie:upper():match(pathsep .. "AREA(%d+)V%.[^" .. pathsep .."]+$"), 10)
if(num ~= nil) then
self.advance_movies[num] = movie
end
--win game movie
if movie:upper():match(pathsep .. "WINGAME%.[^" .. pathsep .. "]+$") then
self.win_movie = movie
end
end
end
--find intro
movies = self.app.fs:listFiles("Intro")
if movies then
for _,movie in pairs(movies) do
if movie:upper():match(pathsep .. "INTRO%.SM4$") then
self.intro_movie = movie
end
end
end
end
function MoviePlayer:playIntro()
self:playMovie(self.intro_movie)
end
function MoviePlayer:playWinMovie()
self:playMovie(self.win_movie)
end
function MoviePlayer:playAdvanceMovie(level)
local filename = self.advance_movies[level]
self.can_skip = false
self.audio:stopBackgroundMusic()
self.holding_bg_music = true
if level == 12 then
self.audio:playSound("DICE122M.WAV")
else
self.audio:playSound("DICEYFIN.WAV")
end
self:playMovie(filename)
end
function MoviePlayer:playLoseMovie()
if #self.lose_movies > 0 then
local filename = self.lose_movies[math.random(#self.lose_movies)]
self:playMovie(filename)
end
end
function MoviePlayer:playMovie(filename)
local x, y, w, h = 0
local screen_w, screen_h = self.app.config.width, self.app.config.height
local ar
if(not self.app.config.movies or filename == nil) then
return
end
self.moviePlayer:load(filename)
if self.moviePlayer:hasAudioTrack() then
self.channel = self.audio:reserveChannel()
self.audio:stopBackgroundMusic()
self.holding_bg_music = true
end
-- calculate target dimensions
local native_w = self.moviePlayer:getNativeWidth()
local native_h = self.moviePlayer:getNativeHeight()
if(native_w ~= 0 and native_h ~= 0) then
ar = native_w / native_h
if(math.abs((screen_w / screen_h) - ar) < 0.001) then
x, y = 0, 0
w, h = screen_w, screen_h
else
if(screen_w > screen_h / native_h * native_w) then
w = screen_h / native_h * native_w
h = screen_h
x = (screen_w - w) / 2
y = 0
else
w = screen_w
h = screen_w / native_w * native_h
x = 0
y = (screen_h - h) / 2
end
end
else
x, y = 0, 0
w, h = screen_w, screen_h
end
self.app.video:startFrame()
self.app.video:fillBlack()
self.app.video:endFrame()
--TODO: Add text e.g. for newspaper headlines
self.moviePlayer:play(x, y, w, h, self.channel)
self.playing = true
end
function MoviePlayer:onMovieAllocatePicture()
self.moviePlayer:allocatePicture()
end
function MoviePlayer:onMovieOver()
self.moviePlayer:unload()
self.app.ui:resetVideo()
if self.channel >= 0 then
self.audio:releaseChannel(self.channel)
self.channel = -1
end
if self.holding_bg_music then
self.audio:resumeBackgroundMusic()
end
-- restore defaults
self.playing = false
self.can_skip = true
end
function MoviePlayer:refresh()
self.moviePlayer:refresh()
end
function MoviePlayer:stop()
if self.can_skip then
self.moviePlayer:stop()
end
end

View File

@@ -415,6 +415,19 @@ function UI:unregisterTextBox(box)
end
end
function UI:resetVideo()
local width, height = self.app.config.width, self.app.config.height
self.app.video:endFrame()
self.app.video = TH.surface(width, height, unpack(self.app.modes))
self.app.gfx:updateTarget(self.app.video)
self.app.video:startFrame()
-- Redraw cursor
local cursor = self.cursor
self.cursor = nil
self:setCursor(cursor)
end
function UI:changeResolution(width, height)
local old_width, old_height = self.app.config.width, self.app.config.height
self.app.video:endFrame()
@@ -552,6 +565,13 @@ function UI:onKeyDown(code, rawchar)
-- Apply key-remapping and normalisation
local key = self.key_codes[code] or rawchar:lower()
if self.app.moviePlayer.playing then
if key == "esc" or key == " " then
self.app.moviePlayer:stop()
end
return true
end
do
local mapped_button = self.key_to_button_remaps[key]
if mapped_button then
@@ -649,6 +669,12 @@ end
function UI:onMouseDown(code, x, y)
local repaint = false
local button = self.button_codes[code] or code
if self.app.moviePlayer.playing then
if button == "left" then
self.app.moviePlayer:stop()
end
return true
end
if self.cursor_entity == nil and self.down_count == 0
and self.cursor == self.default_cursor then
self:setCursor(self.down_cursor)

View File

@@ -1398,6 +1398,7 @@ end
--!param limit (number) [optional] The number the player went over/under which caused him to lose.
function World:loseGame(player_no, reason, limit)
if player_no == 1 then -- TODO: Multiplayer
self.ui.app.moviePlayer:playLoseMovie()
local message = {_S.information.level_lost[1]}
if reason then
message[2] = _S.information.level_lost[2]

View File

@@ -61,6 +61,12 @@ SOFTWARE.
// any music.
#cmakedefine CORSIX_TH_USE_SDL_MIXER
/** Movie options **/
// FFMPEG is used for in game movies. If this library is not present on your
// system, then you can comment out the next line and the game will not have
// movies.
#cmakedefine CORSIX_TH_USE_FFMPEG
/** Font options **/
// FreeType2 can be used for font support beyond the CP437 bitmap fonts which
// come with Theme Hospital. It must be used if translations like Russian or
@@ -84,6 +90,9 @@ SOFTWARE.
#endif
/** Standard includes **/
#ifndef __STDC_CONSTANT_MACROS
# define __STDC_CONSTANT_MACROS
#endif
#include <stddef.h>
#cmakedefine CORSIX_TH_HAS_STDINT_H
#cmakedefine CORSIX_TH_HAS_MALLOC_H

View File

@@ -34,6 +34,10 @@ SOFTWARE.
#define SDL_USEREVENT_MUSIC_OVER (SDL_USEREVENT + 1)
// SDL_USEREVENT_CPCALL - calls lua_cpcall with SDL_Event user.data1 and data2
#define SDL_USEREVENT_CPCALL (SDL_USEREVENT + 2)
// SDL_USEREVENT_ALLOCATE_MOVIE_PICTURE - tells the engine that a picture in the movie picture buffer needs to be allocated
#define SDL_USEREVENT_ALLOCATE_MOVIE_PICTURE (SDL_USEREVENT + 3)
// SDL USEREVENT_MOVIE_OVER - informs script of THMovie movie finishing
#define SDL_USEREVENT_MOVIE_OVER (SDL_USEREVENT + 4)
int luaopen_sdl(lua_State *L);

View File

@@ -214,6 +214,14 @@ static int l_mainloop(lua_State *L)
do_timer = true;
nargs = 0;
break;
case SDL_USEREVENT_ALLOCATE_MOVIE_PICTURE:
lua_pushliteral(dispatcher, "movie_allocate_picture");
nargs = 1;
break;
case SDL_USEREVENT_MOVIE_OVER:
lua_pushliteral(dispatcher, "movie_over");
nargs = 1;
break;
default:
nargs = 0;
break;

View File

@@ -28,6 +28,7 @@ void THLuaRegisterAnims(const THLuaRegisterState_t *pState);
void THLuaRegisterGfx(const THLuaRegisterState_t *pState);
void THLuaRegisterMap(const THLuaRegisterState_t *pState);
void THLuaRegisterSound(const THLuaRegisterState_t *pState);
void THLuaRegisterMovie(const THLuaRegisterState_t *pState);
void THLuaRegisterStrings(const THLuaRegisterState_t *pState);
void THLuaRegisterUI(const THLuaRegisterState_t *pState);
@@ -312,6 +313,7 @@ int luaopen_th(lua_State *L)
THLuaRegisterGfx(pState);
THLuaRegisterAnims(pState);
THLuaRegisterSound(pState);
THLuaRegisterMovie(pState);
THLuaRegisterStrings(pState);
THLuaRegisterUI(pState);

View File

@@ -193,6 +193,11 @@ template <> struct luaT_classinfo<THSoundEffects> {
static inline const char* name() {return "SoundEffects";}
};
class THMovie;
template <> struct luaT_classinfo<THMovie> {
static inline const char* name() {return "Movie";}
};
struct THWindowBase_t;
template <> struct luaT_classinfo<THWindowBase_t> {
static inline const char* name() {return "WindowBase";}

View File

@@ -49,6 +49,7 @@ enum eTHLuaMetatable
MT_Cursor,
MT_SoundArc,
MT_SoundFx,
MT_Movie,
MT_String,
MT_WindowBase,
MT_SpriteList,

View File

@@ -0,0 +1,109 @@
/*
Copyright (c) 2012 Stephen Baker
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "th_lua_internal.h"
#include "th_movie.h"
static int l_movie_new(lua_State *L)
{
luaT_stdnew<THMovie>(L, luaT_environindex, true);
return 1;
}
static int l_movie_load(lua_State *L)
{
THMovie *pMovie = luaT_testuserdata<THMovie>(L);
const char* filepath = lua_tolstring(L, 2, NULL);
pMovie->load(filepath);
return 1;
}
static int l_movie_play(lua_State *L)
{
THMovie *pMovie = luaT_testuserdata<THMovie>(L);
pMovie->play(luaL_checkinteger(L, 2), luaL_checkinteger(L, 3), luaL_checkinteger(L, 4), luaL_checkinteger(L, 5), luaL_checkinteger(L, 6));
return 1;
}
static int l_movie_unload(lua_State *L)
{
THMovie *pMovie = luaT_testuserdata<THMovie>(L);
pMovie->unload();
return 1;
}
static int l_movie_get_native_width(lua_State *L)
{
THMovie *pMovie = luaT_testuserdata<THMovie>(L);
lua_pushinteger(L, pMovie->getNativeWidth());
return 1;
}
static int l_movie_get_native_height(lua_State *L)
{
THMovie *pMovie = luaT_testuserdata<THMovie>(L);
lua_pushinteger(L, pMovie->getNativeHeight());
return 1;
}
static int l_movie_has_audio_track(lua_State *L)
{
THMovie *pMovie = luaT_testuserdata<THMovie>(L);
lua_pushboolean(L, pMovie->hasAudioTrack());
return 1;
}
static int l_movie_allocate_picture(lua_State *L)
{
THMovie *pMovie = luaT_testuserdata<THMovie>(L);
pMovie->allocatePicture();
return 1;
}
static int l_movie_stop(lua_State *L)
{
THMovie *pVideo = luaT_testuserdata<THMovie>(L);
pVideo->stop();
return 1;
}
static int l_movie_refresh(lua_State *L)
{
THMovie *pMovie = luaT_testuserdata<THMovie>(L);
pMovie->refresh();
return 1;
}
void THLuaRegisterMovie(const THLuaRegisterState_t *pState)
{
luaT_class(THMovie, l_movie_new, "moviePlayer", MT_Movie);
luaT_setfunction(l_movie_stop, "stop");
luaT_setfunction(l_movie_allocate_picture, "allocatePicture");
luaT_setfunction(l_movie_refresh, "refresh");
luaT_setfunction(l_movie_load, "load");
luaT_setfunction(l_movie_unload, "unload");
luaT_setfunction(l_movie_play, "play");
luaT_setfunction(l_movie_get_native_width, "getNativeWidth");
luaT_setfunction(l_movie_get_native_height, "getNativeHeight");
luaT_setfunction(l_movie_has_audio_track, "hasAudioTrack");
luaT_endclass();
}

View File

@@ -214,6 +214,22 @@ static int l_soundfx_set_camera(lua_State *L)
return 0;
}
static int l_soundfx_reserve_channel(lua_State *L)
{
int iChannel;
THSoundEffects *pEffects = luaT_testuserdata<THSoundEffects>(L);
iChannel = pEffects->reserveChannel();
lua_pushinteger(L, iChannel);
return 1;
}
static int l_soundfx_release_channel(lua_State *L)
{
THSoundEffects *pEffects = luaT_testuserdata<THSoundEffects>(L);
pEffects->releaseChannel(luaL_checkinteger(L, 2));
return 1;
}
void THLuaRegisterSound(const THLuaRegisterState_t *pState)
{
// Sound Archive
@@ -233,5 +249,7 @@ void THLuaRegisterSound(const THLuaRegisterState_t *pState)
luaT_setfunction(l_soundfx_set_sound_volume, "setSoundVolume");
luaT_setfunction(l_soundfx_set_sound_effects_on, "setSoundEffectsOn");
luaT_setfunction(l_soundfx_set_camera, "setCamera");
luaT_setfunction(l_soundfx_reserve_channel, "reserveChannel");
luaT_setfunction(l_soundfx_release_channel, "releaseChannel");
luaT_endclass();
}

954
CorsixTH/Src/th_movie.cpp Normal file
View File

@@ -0,0 +1,954 @@
/*
Copyright (c) 2012 Stephen Baker
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "th_movie.h"
#include "config.h"
#include "lua_sdl.h"
#ifdef CORSIX_TH_USE_FFMPEG
#include "th_gfx.h"
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
}
#include <SDL_mixer.h>
#include <iostream>
#define INBUF_SIZE 4096
#define AUDIO_BUFFER_SIZE 1024
int th_movie_stream_reader_thread(void* pState)
{
THMovie *pMovie = (THMovie *)pState;
pMovie->readStreams();
return 0;
}
int th_movie_video_thread(void* pState)
{
THMovie *pMovie = (THMovie *)pState;
pMovie->runVideo();
return 0;
}
void th_movie_audio_callback(int iChannel, void *pStream, int iStreamSize, void *pUserData)
{
THMovie *pMovie = (THMovie *)pUserData;
pMovie->copyAudioToStream((Uint8*)pStream, iStreamSize);
}
THMoviePicture::THMoviePicture():
m_fAllocated(false),
m_pOverlay(NULL),
m_fReallocate(false),
m_pixelFormat(PIX_FMT_YUV420P) {}
THMoviePicture::~THMoviePicture()
{
if(m_pOverlay)
{
SDL_FreeYUVOverlay(m_pOverlay);
m_pOverlay = NULL;
}
}
void THMoviePicture::allocate()
{
SDL_Surface* pSurface = SDL_GetVideoSurface();
if(m_pOverlay)
{
SDL_FreeYUVOverlay(m_pOverlay);
}
m_pOverlay = SDL_CreateYUVOverlay(m_iWidth, m_iHeight, SDL_YV12_OVERLAY, pSurface);
if(m_pOverlay == NULL || m_pOverlay->pitches[0] < m_iWidth)
{
std::cerr << "Problem creating overlay: " << SDL_GetError() << "\n";
return;
}
}
void THMoviePicture::deallocate()
{
if(m_pOverlay)
{
SDL_FreeYUVOverlay(m_pOverlay);
m_pOverlay = NULL;
}
}
void THMoviePicture::draw()
{
SDL_Rect rcDest;
int iError;
rcDest.x = m_iX;
rcDest.y = m_iY;
rcDest.w = m_iWidth;
rcDest.h = m_iHeight;
if(m_pOverlay)
{
iError = SDL_DisplayYUVOverlay(m_pOverlay, &rcDest);
if(iError < 0)
{
std::cerr << "Error displaying overlay: " << SDL_GetError();
}
}
}
THAVPacketQueue::THAVPacketQueue():
iCount(0),
m_pFirstPacket(NULL),
m_pLastPacket(NULL)
{
m_pMutex = SDL_CreateMutex();
m_pCond = SDL_CreateCond();
}
THAVPacketQueue::~THAVPacketQueue()
{
SDL_DestroyCond(m_pCond);
SDL_DestroyMutex(m_pMutex);
}
int THAVPacketQueue::getCount()
{
return iCount;
}
void THAVPacketQueue::push(AVPacket *pPacket)
{
AVPacketList* pNode;
if(av_dup_packet(pPacket) < 0) { throw -1; }
pNode = (AVPacketList*)av_malloc(sizeof(AVPacketList));
pNode->pkt = *pPacket;
pNode->next = NULL;
SDL_LockMutex(m_pMutex);
if(m_pLastPacket == NULL)
{
m_pFirstPacket = pNode;
}
else
{
m_pLastPacket->next = pNode;
}
m_pLastPacket = pNode;
iCount++;
SDL_CondSignal(m_pCond);
SDL_UnlockMutex(m_pMutex);
}
AVPacket* THAVPacketQueue::pull(bool fBlock)
{
AVPacketList *pNode;
AVPacket *pPacket;
SDL_LockMutex(m_pMutex);
pNode = m_pFirstPacket;
if(pNode == NULL && fBlock)
{
SDL_CondWait(m_pCond, m_pMutex);
pNode = m_pFirstPacket;
}
if(pNode == NULL)
{
pPacket = NULL;
}
else
{
m_pFirstPacket = pNode->next;
if(m_pFirstPacket == NULL) { m_pLastPacket = NULL; }
iCount--;
pPacket = (AVPacket*)av_malloc(sizeof(AVPacket));
*pPacket = pNode->pkt;
av_free(pNode);
}
SDL_UnlockMutex(m_pMutex);
return pPacket;
}
void THAVPacketQueue::release()
{
SDL_LockMutex(m_pMutex);
SDL_CondSignal(m_pCond);
SDL_UnlockMutex(m_pMutex);
}
THMovie::THMovie():
m_pFormatContext(NULL),
m_pVideoCodecContext(NULL),
m_pAudioCodecContext(NULL),
m_pSwrContext(NULL),
m_iChannel(-1),
m_pVideoQueue(NULL),
m_pAudioQueue(NULL),
m_pSwsContext(NULL),
m_pChunk(NULL),
m_frame(NULL),
m_pStreamThread(NULL),
m_pVideoThread(NULL),
m_iAudioBufferMaxSize(0),
m_iAudioBufferSize(0),
m_pAudioPacket(NULL)
{
av_register_all();
m_flushPacket = (AVPacket*)av_malloc(sizeof(AVPacket));
av_init_packet(m_flushPacket);
m_flushPacket->data = (uint8_t *)"FLUSH";
m_flushPacket->size = 5;
m_pPictureQueueCond = SDL_CreateCond();
m_pPictureQueueMutex = SDL_CreateMutex();
m_pbChunkBuffer = (Uint8*)malloc(AUDIO_BUFFER_SIZE);
memset(m_pbChunkBuffer, 0, AUDIO_BUFFER_SIZE);
}
THMovie::~THMovie()
{
unload();
av_free_packet(m_flushPacket);
av_free(m_flushPacket);
free(m_pbChunkBuffer);
SDL_DestroyCond(m_pPictureQueueCond);
SDL_DestroyMutex(m_pPictureQueueMutex);
sws_freeContext(m_pSwsContext);
}
void THMovie::load(const char* szFilepath)
{
int iError = 0;
unload(); //Unload any currently loaded video to free memory
m_fAborting = false;
iError = avformat_open_input(&m_pFormatContext, szFilepath, NULL, NULL);
if(iError < 0)
{
std::cerr << "Failed to open av input: " << iError;
return;
}
iError = avformat_find_stream_info(m_pFormatContext, NULL);
if(iError < 0)
{
std::cerr << "Failed to find stream info: " << iError;
return;
}
m_iVideoStream = av_find_best_stream(m_pFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &m_pVideoCodec, 0);
m_pVideoCodecContext = m_pFormatContext->streams[m_iVideoStream]->codec;
avcodec_open2(m_pVideoCodecContext, m_pVideoCodec, NULL);
m_iAudioStream = av_find_best_stream(m_pFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, &m_pAudioCodec, 0);
if(m_iAudioStream >= 0)
{
m_pAudioCodecContext = m_pFormatContext->streams[m_iAudioStream]->codec;
avcodec_open2(m_pAudioCodecContext, m_pAudioCodec, NULL);
}
}
int THMovie::getNativeHeight()
{
int iHeight = 0;
if(m_pVideoCodecContext)
{
iHeight = m_pVideoCodecContext->height;
}
return iHeight;
}
int THMovie::getNativeWidth()
{
int iWidth = 0;
if(m_pVideoCodecContext)
{
iWidth = m_pVideoCodecContext->width;
}
return iWidth;
}
bool THMovie::hasAudioTrack()
{
return (m_iAudioStream >= 0);
}
void THMovie::play(int iX, int iY, int iWidth, int iHeight, int iChannel)
{
m_iX = iX;
m_iY = iY;
m_iWidth = iWidth;
m_iHeight = iHeight;
#ifdef CORSIX_TH_USE_OGL_RENDERER
SDL_Surface* pSurface = SDL_GetVideoSurface();
SDL_SetVideoMode(pSurface->w, pSurface->h, 0, pSurface->flags & ~SDL_OPENGL);
#endif
m_frame = NULL;
m_pVideoQueue = new THAVPacketQueue();
m_iPictureQueueSize = 0;
m_iPictureQueueReadIndex = 0;
m_iPictureQueueWriteIndex = 0;
m_pAudioPacket = NULL;
m_iAudioPacketSize = 0;
m_pbAudioPacketData = NULL;
m_iAudioBufferSize = 0;
m_iAudioBufferIndex = 0;
m_iAudioBufferMaxSize = 0;
m_pAudioQueue = new THAVPacketQueue();
m_iStartTime = SDL_GetTicks();
if(m_iAudioStream >= 0)
{
Mix_QuerySpec(&m_iMixerFrequency, NULL, &m_iMixerChannels);
m_pSwrContext = swr_alloc_set_opts(
m_pSwrContext,
m_iMixerChannels==1?AV_CH_LAYOUT_MONO:AV_CH_LAYOUT_STEREO,
AV_SAMPLE_FMT_S16,
m_iMixerFrequency,
m_pAudioCodecContext->channel_layout,
m_pAudioCodecContext->sample_fmt,
m_pAudioCodecContext->sample_rate,
0,
NULL);
swr_init(m_pSwrContext);
m_pChunk = Mix_QuickLoad_RAW(m_pbChunkBuffer, AUDIO_BUFFER_SIZE);
m_iChannel = Mix_PlayChannel(iChannel, m_pChunk, -1);
if(m_iChannel < 0)
{
m_iChannel = -1;
std::cerr << "Error on Mix_PlayChannel for th_movie";
}
else
{
Mix_RegisterEffect(m_iChannel, th_movie_audio_callback, NULL, this);
}
}
m_pStreamThread = SDL_CreateThread(th_movie_stream_reader_thread, this);
m_pVideoThread = SDL_CreateThread(th_movie_video_thread, this);
}
void THMovie::stop()
{
m_fAborting = true;
}
void THMovie::unload()
{
m_fAborting = true;
if(m_pAudioQueue)
{
m_pAudioQueue->release();
}
if(m_pVideoQueue)
{
m_pVideoQueue->release();
}
SDL_LockMutex(m_pPictureQueueMutex);
SDL_CondSignal(m_pPictureQueueCond);
SDL_UnlockMutex(m_pPictureQueueMutex);
if(m_pStreamThread)
{
SDL_WaitThread(m_pStreamThread, NULL);
m_pStreamThread = NULL;
}
if(m_pVideoThread)
{
SDL_WaitThread(m_pVideoThread, NULL);
m_pVideoThread = NULL;
}
//wait until after other threads are closed to clear the packet queues
//so we don't free something being used.
if(m_pAudioQueue)
{
while(m_pAudioQueue->getCount() > 0)
{
AVPacket* p = m_pAudioQueue->pull(false);
av_free_packet(p);
}
delete m_pAudioQueue;
m_pAudioQueue = NULL;
}
if(m_pVideoQueue)
{
while(m_pVideoQueue->getCount() > 0)
{
AVPacket* p = m_pVideoQueue->pull(false);
av_free_packet(p);
}
delete m_pVideoQueue;
m_pVideoQueue = NULL;
}
for(int i=0; i<PICTURE_BUFFER_SIZE; i++)
{
m_aPictureQueue[i].deallocate();
m_aPictureQueue[i].m_fAllocated = false;
}
if(m_iChannel >= 0)
{
Mix_UnregisterAllEffects(m_iChannel);
Mix_HaltChannel(m_iChannel);
Mix_FreeChunk(m_pChunk);
m_iChannel = -1;
}
if(m_iAudioBufferMaxSize > 0)
{
av_free(m_pbAudioBuffer);
m_iAudioBufferMaxSize = 0;
}
if(m_pVideoCodecContext)
{
avcodec_close(m_pVideoCodecContext);
m_pVideoCodecContext = NULL;
}
if(m_pAudioCodecContext)
{
avcodec_close(m_pAudioCodecContext);
m_pAudioCodecContext = NULL;
}
if(m_pFormatContext)
{
avformat_close_input(&m_pFormatContext);
}
if(m_frame)
{
av_free(m_frame);
m_frame = NULL;
}
swr_free(&m_pSwrContext);
if(m_pAudioPacket)
{
m_pAudioPacket->data = m_pbAudioPacketData;
m_pAudioPacket->size = m_iAudioPacketSize;
av_free_packet(m_pAudioPacket);
av_free(m_pAudioPacket);
m_pAudioPacket = NULL;
m_pbAudioPacketData = NULL;
m_iAudioPacketSize = 0;
}
#ifdef CORSIX_TH_USE_OGL_RENDERER
SDL_Surface* pSurface = SDL_GetVideoSurface();
SDL_SetVideoMode(pSurface->w, pSurface->h, 0, pSurface->flags | SDL_OPENGL);
#endif
}
void THMovie::readStreams()
{
AVPacket packet;
int iError;
while(!m_fAborting)
{
iError = av_read_frame(m_pFormatContext, &packet);
if(iError < 0)
{
if(iError == AVERROR_EOF || m_pFormatContext->pb->error || m_pFormatContext->pb->eof_reached)
{
break;
}
}
else
{
if(packet.stream_index == m_iVideoStream)
{
m_pVideoQueue->push(&packet);
}
else if (packet.stream_index == m_iAudioStream)
{
m_pAudioQueue->push(&packet);
}
else
{
av_free_packet(&packet);
}
}
}
while(!m_fAborting)
{
if(m_pVideoQueue->getCount() == 0 && m_pAudioQueue->getCount() == 0 && m_iPictureQueueSize == 0)
{
break;
}
SDL_Delay(10);
}
SDL_Event endEvent;
endEvent.type = SDL_USEREVENT_MOVIE_OVER;
SDL_PushEvent(&endEvent);
m_fAborting = true;
}
void THMovie::refresh()
{
THMoviePicture* pMoviePicture;
if(m_pFormatContext->streams[m_iVideoStream])
{
if(m_iPictureQueueSize > 0)
{
pMoviePicture = &(m_aPictureQueue[m_iPictureQueueReadIndex]);
double curTime = SDL_GetTicks() - m_iStartTime;
//don't play a frame too early
if(pMoviePicture->m_dPts > 0)
{
if(pMoviePicture->m_dPts * 1000.0 > curTime)
{
return;
}
}
if(m_iPictureQueueSize > 1)
{
THMoviePicture* nextPicture = &(m_aPictureQueue[(m_iPictureQueueReadIndex + 1) % PICTURE_BUFFER_SIZE]);
//if we have another frame and it's time has already passed, drop the current frame
if(nextPicture->m_dPts > 0 && nextPicture->m_dPts * 1000.0 < curTime)
{
advancePictureQueue();
}
}
pMoviePicture->draw();
advancePictureQueue();
}
}
}
void THMovie::advancePictureQueue()
{
m_iPictureQueueReadIndex++;
if(m_iPictureQueueReadIndex == PICTURE_BUFFER_SIZE)
{
m_iPictureQueueReadIndex = 0;
}
SDL_LockMutex(m_pPictureQueueMutex);
m_iPictureQueueSize--;
SDL_CondSignal(m_pPictureQueueCond);
SDL_UnlockMutex(m_pPictureQueueMutex);
}
void THMovie::runVideo()
{
AVFrame *pFrame = avcodec_alloc_frame();
int64_t iStreamPts = AV_NOPTS_VALUE;
double dClockPts;
int iError;
while(!m_fAborting)
{
avcodec_get_frame_defaults(pFrame);
iError = getVideoFrame(pFrame, &iStreamPts);
if(iError < 0)
{
break;
}
else if(iError == 0)
{
continue;
}
dClockPts = iStreamPts * av_q2d(m_pVideoCodecContext->time_base);
iError = queuePicture(pFrame, dClockPts);
if(iError < 0)
{
break;
}
}
avcodec_flush_buffers(m_pVideoCodecContext);
av_free(pFrame);
}
int THMovie::getVideoFrame(AVFrame *pFrame, int64_t *piPts)
{
int iGotPicture = 0;
int iError;
AVPacket *pPacket = m_pVideoQueue->pull(true);
if(pPacket == NULL)
{
return -1;
}
if(pPacket->data == m_flushPacket->data)
{
//TODO: Flush
return 0;
}
iError = avcodec_decode_video2(m_pVideoCodecContext, pFrame, &iGotPicture, pPacket);
av_free_packet(pPacket);
av_free(pPacket);
if(iError < 0)
{
return 0;
}
if(iGotPicture)
{
iError = 1;
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(54, 18, 100)
*piPts = *(int64_t*)av_opt_ptr(avcodec_get_frame_class(), pFrame, "best_effort_timestamp");
#else
*piPts = av_frame_get_best_effort_timestamp(pFrame);
#endif
if(*piPts == AV_NOPTS_VALUE)
{
*piPts = 0;
}
return iError;
}
return 0;
}
int THMovie::queuePicture(AVFrame *pFrame, double dPts)
{
SDL_LockMutex(m_pPictureQueueMutex);
while(!m_fAborting && m_iPictureQueueSize >= PICTURE_BUFFER_SIZE - 2)
{
SDL_CondWait(m_pPictureQueueCond, m_pPictureQueueMutex);
}
SDL_UnlockMutex(m_pPictureQueueMutex);
if(m_fAborting)
{
return -1;
}
THMoviePicture* pMoviePicture = &m_aPictureQueue[m_iPictureQueueWriteIndex];
//check if overlay needs to be allocated or changed
if(!pMoviePicture->m_pOverlay ||
!pMoviePicture->m_fAllocated ||
pMoviePicture->m_fReallocate ||
pMoviePicture->m_iWidth != m_iWidth ||
pMoviePicture->m_iHeight != m_iHeight)
{
SDL_Event reallocEvent;
pMoviePicture->m_fAllocated = false;
pMoviePicture->m_fReallocate = false;
pMoviePicture->m_iWidth = m_iWidth;
pMoviePicture->m_iHeight = m_iHeight;
pMoviePicture->m_iX = m_iX;
pMoviePicture->m_iY = m_iY;
//the actual allocation needs to be done on the main thread or else puppies may die.
reallocEvent.type = SDL_USEREVENT_ALLOCATE_MOVIE_PICTURE;
reallocEvent.user.data1 = this;
SDL_PushEvent(&reallocEvent);
//wait for the main tread
SDL_LockMutex(m_pPictureQueueMutex);
while(!m_fAborting && !pMoviePicture->m_fAllocated)
{
SDL_CondWait(m_pPictureQueueCond, m_pPictureQueueMutex);
}
SDL_UnlockMutex(m_pPictureQueueMutex);
if(m_fAborting)
{
return -1;
}
}
if(pMoviePicture->m_pOverlay)
{
AVPicture picture = {};
SDL_LockYUVOverlay(pMoviePicture->m_pOverlay);
picture.data[0] = pMoviePicture->m_pOverlay->pixels[0];
picture.data[1] = pMoviePicture->m_pOverlay->pixels[2];
picture.data[2] = pMoviePicture->m_pOverlay->pixels[1];
picture.linesize[0] = pMoviePicture->m_pOverlay->pitches[0];
picture.linesize[1] = pMoviePicture->m_pOverlay->pitches[2];
picture.linesize[2] = pMoviePicture->m_pOverlay->pitches[1];
m_pSwsContext = sws_getCachedContext(m_pSwsContext, pFrame->width, pFrame->height, (PixelFormat)pFrame->format, pMoviePicture->m_iWidth, pMoviePicture->m_iHeight, pMoviePicture->m_pixelFormat, SWS_BICUBIC, NULL, NULL, NULL);
if(m_pSwsContext == NULL)
{
std::cerr << "Failed to initialize SwsContext";
return 1;
}
sws_scale(m_pSwsContext, pFrame->data, pFrame->linesize, 0, pMoviePicture->m_iHeight, picture.data, picture.linesize);
SDL_UnlockYUVOverlay(pMoviePicture->m_pOverlay);
pMoviePicture->m_dPts = dPts;
m_iPictureQueueWriteIndex++;
if(m_iPictureQueueWriteIndex == PICTURE_BUFFER_SIZE)
{
m_iPictureQueueWriteIndex = 0;
}
SDL_LockMutex(m_pPictureQueueMutex);
m_iPictureQueueSize++;
SDL_UnlockMutex(m_pPictureQueueMutex);
}
return 0;
}
void THMovie::allocatePicture()
{
THMoviePicture *pMoviePicture = &m_aPictureQueue[m_iPictureQueueWriteIndex];
pMoviePicture->allocate();
SDL_LockMutex(m_pPictureQueueMutex);
pMoviePicture->m_fAllocated = true;
SDL_CondSignal(m_pPictureQueueCond);
SDL_UnlockMutex(m_pPictureQueueMutex);
}
void THMovie::copyAudioToStream(Uint8 *pbStream, int iStreamSize)
{
int iAudioSize;
int iCopyLength;
while(iStreamSize > 0)
{
if(m_iAudioBufferIndex >= m_iAudioBufferSize)
{
iAudioSize = decodeAudioFrame();
if(iAudioSize <= 0)
{
memset(m_pbAudioBuffer, 0, m_iAudioBufferSize);
}
else
{
m_iAudioBufferSize = iAudioSize;
}
m_iAudioBufferIndex = 0;
}
iCopyLength = m_iAudioBufferSize - m_iAudioBufferIndex;
if(iCopyLength > iStreamSize) { iCopyLength = iStreamSize; }
memcpy(pbStream, (uint8_t *)m_pbAudioBuffer + m_iAudioBufferIndex, iCopyLength);
iStreamSize -= iCopyLength;
pbStream += iCopyLength;
m_iAudioBufferIndex += iCopyLength;
}
}
int THMovie::decodeAudioFrame()
{
int iBytesConsumed = 0;
int iSampleSize = 0;
int iOutSamples;
int iGotFrame = 0;
bool fNewPacket = false;
bool fFlushComplete = false;
while(!iGotFrame && !m_fAborting)
{
if(!m_pAudioPacket || m_pAudioPacket->size == 0)
{
if(m_pAudioPacket)
{
m_pAudioPacket->data = m_pbAudioPacketData;
m_iAudioPacketSize = m_iAudioPacketSize;
av_free_packet(m_pAudioPacket);
av_free(m_pAudioPacket);
m_pAudioPacket = NULL;
}
m_pAudioPacket = m_pAudioQueue->pull(true);
if(m_fAborting)
{
break;
}
m_pbAudioPacketData = m_pAudioPacket->data;
m_iAudioPacketSize = m_pAudioPacket->size;
if(m_pAudioPacket == NULL)
{
fNewPacket = false;
return -1;
}
fNewPacket = true;
if(m_pAudioPacket->data == m_flushPacket->data)
{
avcodec_flush_buffers(m_pAudioCodecContext);
fFlushComplete = false;
}
}
while(m_pAudioPacket->size > 0 || (!m_pAudioPacket->data && fNewPacket))
{
if(!m_frame)
{
m_frame = avcodec_alloc_frame();
}
else
{
avcodec_get_frame_defaults(m_frame);
}
if(fFlushComplete)
{
break;
}
fNewPacket = false;
iBytesConsumed = avcodec_decode_audio4(m_pAudioCodecContext, m_frame, &iGotFrame, m_pAudioPacket);
if(iBytesConsumed < 0)
{
m_pAudioPacket->size = 0;
break;
}
m_pAudioPacket->data += iBytesConsumed;
m_pAudioPacket->size -= iBytesConsumed;
if(!iGotFrame)
{
if(m_pAudioPacket->data && m_pAudioCodecContext->codec->capabilities & CODEC_CAP_DELAY)
{
fFlushComplete = true;
}
}
}
}
//TODO: Compensate for audio being out of sync
#if LIBSWRESAMPLE_VERSION_INT < AV_VERSION_INT(0, 12, 100)
//over-estimate output samples
iOutSamples = (int)av_rescale_rnd(m_frame->nb_samples, m_iMixerFrequency, m_pAudioCodecContext->sample_rate, AV_ROUND_UP);
iSampleSize = av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) * iOutSamples * m_iMixerChannels;
if(iSampleSize > m_iAudioBufferMaxSize)
{
if(m_iAudioBufferMaxSize > 0)
{
av_free(m_pbAudioBuffer);
}
m_pbAudioBuffer = (uint8_t*)av_malloc(iSampleSize);
m_iAudioBufferMaxSize = iSampleSize;
}
#else
//output samples = (input samples + delay) * output rate / input rate
iOutSamples = (int)av_rescale_rnd(
swr_get_delay(
m_pSwrContext,
m_pAudioCodecContext->sample_rate) + m_frame->nb_samples,
m_iMixerFrequency,
m_pAudioCodecContext->sample_rate,
AV_ROUND_UP);
iSampleSize = av_samples_get_buffer_size(NULL, m_iMixerChannels, iOutSamples, AV_SAMPLE_FMT_S16, 0);
if(iSampleSize > m_iAudioBufferMaxSize)
{
if(m_iAudioBufferMaxSize > 0)
{
av_free(m_pbAudioBuffer);
}
av_samples_alloc(&m_pbAudioBuffer, NULL, m_iMixerChannels, iOutSamples, AV_SAMPLE_FMT_S16, 0);
m_iAudioBufferMaxSize = iSampleSize;
}
#endif
swr_convert(m_pSwrContext, &m_pbAudioBuffer, iOutSamples, (const uint8_t**)&m_frame->data[0], m_frame->nb_samples);
return iSampleSize;
}
#else //CORSIX_TH_USE_FFMPEG
THMovie::THMovie() {}
THMovie::~THMovie() {}
void THMovie::load(const char* filepath) {}
void THMovie::unload() {}
void THMovie::play(int iX, int iY, int iWidth, int iHeight, int iChannel)
{
SDL_Event endEvent;
endEvent.type = SDL_USEREVENT_MOVIE_OVER;
SDL_PushEvent(&endEvent);
}
void THMovie::stop() {}
int THMovie::getNativeHeight() { return 0; }
int THMovie::getNativeWidth() { return 0; }
bool THMovie::hasAudioTrack() { return false; }
void THMovie::refresh() {}
void THMovie::copyAudioToStream(Uint8 *stream, int length) {}
void THMovie::runVideo() {}
void THMovie::allocatePicture() {}
void THMovie::readStreams() {}
#endif //CORSIX_TH_USE_FFMPEG

160
CorsixTH/Src/th_movie.h Normal file
View File

@@ -0,0 +1,160 @@
/*
Copyright (c) 2012 Stephen Baker
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef TH_VIDEO_H
#define TH_VIDEO_H
#define PICTURE_BUFFER_SIZE 4
#include <string>
#include <queue>
#include "config.h"
#include "th_gfx.h"
#include "th_sound.h"
#include "SDL_mixer.h"
#ifdef CORSIX_TH_USE_FFMPEG
extern "C"
{
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
}
class THMoviePicture
{
public:
SDL_Overlay *m_pOverlay;
int m_iX, m_iY, m_iWidth, m_iHeight;
double m_dPts;
bool m_fAllocated;
bool m_fReallocate;
PixelFormat m_pixelFormat;
THMoviePicture();
~THMoviePicture();
void allocate();
void deallocate();
void draw();
};
class THAVPacketQueue
{
public:
THAVPacketQueue();
~THAVPacketQueue();
void push(AVPacket *packet);
AVPacket* pull(bool block);
int getCount();
void release();
private:
AVPacketList *m_pFirstPacket, *m_pLastPacket;
int iCount;
SDL_mutex *m_pMutex;
SDL_cond *m_pCond;
};
#endif //CORSIX_TH_USE_FFMPEG
class THMovie
{
public:
THMovie();
~THMovie();
void load(const char* szFilepath);
void unload();
void play(int iX, int iY, int iWidth, int iHeight, int iChannel);
void stop();
int getNativeHeight();
int getNativeWidth();
bool hasAudioTrack();
void refresh();
void copyAudioToStream(Uint8 *pbStream, int iStreamSize);
void runVideo();
void allocatePicture();
void readStreams();
protected:
#ifdef CORSIX_TH_USE_FFMPEG
int decodeAudioFrame();
int getVideoFrame(AVFrame *pFrame, int64_t *piPts);
int queuePicture(AVFrame *pFrame, double dPts);
void advancePictureQueue();
int m_iX;
int m_iY;
int m_iWidth;
int m_iHeight;
AVFormatContext* m_pFormatContext;
int m_iVideoStream;
int m_iAudioStream;
AVCodec* m_pVideoCodec;
AVCodec* m_pAudioCodec;
AVCodecContext* m_pVideoCodecContext;
AVCodecContext* m_pAudioCodecContext;
THAVPacketQueue *m_pVideoQueue;
SDL_mutex *m_pPictureQueueMutex;
SDL_cond *m_pPictureQueueCond;
THMoviePicture m_aPictureQueue[PICTURE_BUFFER_SIZE];
int m_iPictureQueueWriteIndex;
int m_iPictureQueueReadIndex;
int m_iPictureQueueSize;
//tick when video was started in ms
int m_iStartTime;
THAVPacketQueue *m_pAudioQueue;
//audio buffer
int m_iAudioBufferSize;
int m_iAudioBufferIndex;
int m_iAudioBufferMaxSize;
uint8_t* m_pbAudioBuffer;
int m_iChannel;
Uint8* m_pbChunkBuffer;
Mix_Chunk* m_pChunk;
int m_iMixerChannels;
int m_iMixerFrequency;
SwrContext* m_pSwrContext;
bool m_fAborting;
AVPacket* m_pAudioPacket;
int m_iAudioPacketSize;
uint8_t *m_pbAudioPacketData;
AVPacket* m_flushPacket;
AVFrame* m_frame;
SwsContext* m_pSwsContext;
SDL_Thread* m_pStreamThread;
SDL_Thread* m_pRefreshThread;
SDL_Thread* m_pVideoThread;
#endif //CORSIX_TH_USE_FFMPEG
};
#endif // TH_VIDEO_H

View File

@@ -198,7 +198,7 @@ void THSoundEffects::_onChannelFinish(int iChannel)
if(pThis == NULL)
return;
pThis->m_iChannelStatus |= (1 << iChannel);
pThis->releaseChannel(iChannel);
}
THSoundEffects* THSoundEffects::getSingleton()
@@ -274,13 +274,25 @@ void THSoundEffects::setSoundEffectsOn(bool bOn)
m_bSoundEffectsOn = bOn;
}
void THSoundEffects::_playRaw(size_t iIndex, int iVolume)
int THSoundEffects::reserveChannel()
{
// NB: Callers ensure that m_iChannelStatus != 0
int iChannel = 0;
for(; (m_iChannelStatus & (1 << iChannel)) == 0; ++iChannel) {}
m_iChannelStatus &=~ (1 << iChannel);
return iChannel;
}
void THSoundEffects::releaseChannel(int iChannel)
{
m_iChannelStatus |= (1 << iChannel);
}
void THSoundEffects::_playRaw(size_t iIndex, int iVolume)
{
int iChannel = reserveChannel();
Mix_Volume(iChannel, iVolume);
Mix_PlayChannelTimed(iChannel, m_ppSounds[iIndex], 0, -1);
}

View File

@@ -104,6 +104,8 @@ public:
void setSoundEffectsVolume(double dVolume);
void setSoundEffectsOn(bool bOn);
void setCamera(int iX, int iY, int iRadius);
int reserveChannel();
void releaseChannel(int iChannel);
protected:
#ifdef CORSIX_TH_USE_SDL_MIXER