Hey guys I just wrote a replay recorder script in lua and it works pretty well so far. It either captures a new replay or plays an existing one.
Why did I do this?
I wanted to take fs2 cinematics to the next level. Trust me it was a lot of work - about 20 hours. Now it would be possible to fly with a ship manually and later play it back with camera movements made in fred. Ideally a different person could record some battle scenes or what not and someone else could play them, fraps them and create a movie.
Currently the velocity, rotational velocity, target, targetsubsystem and primary fire of all ships are saved. I fail to find a way to capture if a secondary or cm was fired. As you can see in scripting.html there is a PrimaryTriggerDown() function inside the ship class but none for
secondary fire or countermeasures. I think the needed functions need to be implemented to lua first. Also the
firePrimary() function of the ship class does not work. Currently I just display the message "PEW" instead of firing
... Anyone interested in fixing those please? =)
I am working on a very slow computer now and it doesnt hurt the performance. Well I only tested it with 3 ships in a mission so if you record a huge battle with about 50 ships it may be slower I dont know. Only the loading time (if you play a recorded file) and the closing time (if you record a new file) of a mission are somewhat longer than usual.
scrip here:
#Conditional Hooks
$Application: FS2_Open
$On Mission Start:
[
--- find keyword and return the string before it and after it
function find_keyword(text, keyword)
-- find any instances of the keyword
local key_s, key_e = text:find(keyword)
-- if we cant find a thing
if key_s == nil then
return nil
end
return text:sub(1, key_s - 1), text:sub(key_s + 1)
end
function split(text, splitter)
local values = {}
local strStart, strEnd = find_keyword(text, splitter)
local lastEnd = text
while strStart do
table.insert(values, strStart)
lastEnd = strEnd
strStart, strEnd = find_keyword(strEnd, splitter)
end
table.insert(values, lastEnd)
return values
end
function add_entry(shipName, shipVelocity, shipRotationalVelocity, shipPrimaryFire, shipTarget, shipTargetSubSystem)
--add ship values
table.insert(recordTable[#recordTable].ships, {
["name"]=shipName,
["velocity"]=shipVelocity,
["rotationalVelocity"]=shipRotationalVelocity,
["primaryFire"]=shipPrimaryFire,
["target"]=shipTarget,
["targetSubSystem"]=shipTargetSubSystem,
})
end
function load_file(filename, filepath)
local file = cf.openFile(filename, "r", filepath)
while true do
local line = file:read("*l")
if line == nil then
break
end
local strStart, strEnd = find_keyword(line, ":")
local missionTime = tonumber(strStart)
--create array of ship values
table.insert(recordTable, {
["missionTime"] = missionTime,
["ships"] = {},
})
local ships = split(strEnd, "#")
local countShips = #ships
for i=1, countShips do
local values = split(ships[i], "/")
if (#values >= 6) then
local shipName = values[1]
local velocityValues = split(values[2], ",")
local shipVelocity = ba.createVector(
tonumber(velocityValues[1]),
tonumber(velocityValues[2]),
tonumber(velocityValues[3])
)
local rotationalVelocityValues = split(values[3], ",")
local shipRotationalVelocity = ba.createVector(
tonumber(rotationalVelocityValues[1]),
tonumber(rotationalVelocityValues[2]),
tonumber(rotationalVelocityValues[3])
)
local shipPrimaryFire
if (values[4] == "1") then
shipPrimaryFire = true
else
shipPrimaryFire = false
end
local shipTarget = values[5]
local shipTargetSubSystem = values[6]
add_entry(shipName, shipVelocity, shipRotationalVelocity, shipPrimaryFire, shipTarget, shipTargetSubSystem)
end
end
end
file:close()
end
function freeze_ai()
--freezes ai of all ships (set orders to play dead)
local countShips = #mn.Ships
for i=1, countShips do
mn.Ships[i]:clearOrders()
mn.Ships[i]:giveOrder(ORDER_PLAY_DEAD)
end
end
--[[
created by stfx - 1/5/2010
20 hours of work
!only edit script here!
RECORD_PATH ... path that contains the recorded file
RECORD_STATIC_NAME ... saved filename should be record_1.cfg (true) or record_????.cfg (false)
PLAY_MODE ... playing recorded file (true) or recording a new one (false)
SCRIPT_ACTIVATED ... script is being executed (true) or not (false)
]]
RECORD_PATH = "data/tables/"
RECORD_STATIC_NAME = false
PLAY_MODE = false
SCRIPT_ACTIVATED = true
if (SCRIPT_ACTIVATED == true) then
--init arrays
recordTable = {}
if (PLAY_MODE == true) then
--load record file
load_file("record_1.cfg", RECORD_PATH)
freeze_ai()
lastTime = 0
playIndex = 1
end
end
]
$State: GS_STATE_GAME_PLAY
$On Frame:
[
function round(num)
return math.floor(num * 100) / 100
end
function add_entry(shipName, shipVelocity, shipRotationalVelocity, shipPrimaryFire, shipTarget, shipTargetSubSystem)
--add ship values
table.insert(recordTable[#recordTable].ships, {
["name"]=shipName,
["velocity"]=shipVelocity,
["rotationalVelocity"]=shipRotationalVelocity,
["primaryFire"]=shipPrimaryFire,
["target"]=shipTarget,
["targetSubSystem"]=shipTargetSubSystem,
})
end
function record_ship(ship)
--ship values
local shipName = ship.Name
local shipVelocity = ship.Physics.Velocity
local shipRotationalVelocity = ship.Physics.RotationalVelocity
local shipPrimaryFire = ship.PrimaryTriggerDown
local shipTargetSubSystem = ship.TargetSubsystem.Name
local shipTarget
if (ship.Target:isValid()) then
shipTarget = ship.Target.Name
end
add_entry(shipName, shipVelocity, shipRotationalVelocity, shipPrimaryFire, shipTarget, shipTargetSubSystem)
end
function record(missionTime)
--record each ship
local countShips = #mn.Ships
if (countShips > 0) then
local entriesCount = #recordTable
if (entriesCount > 0 and recordTable[entriesCount].missionTime == missionTime) then
return
end
--create array of ship values
table.insert(recordTable, {
["missionTime"] = missionTime,
["ships"] = {},
})
for i=1, countShips do
local objectShip = mn.Ships[i]
--only record if still alive
if (objectShip.HitpointsLeft > 0) then
record_ship(objectShip)
end
end
end
end
function smooth_value(x, y, lastTime, nextTime, curTime)
local stepCount = (nextTime - lastTime) / 0.01
local curStep = stepCount - (nextTime - curTime) / 0.01
local interval
local smallest
if (x < y) then
smallest = x
interval = (y - x) / stepCount
elseif (x > y) then
smallest = y
interval = (x - y) / stepCount
else
smallest = x
interval = 0
end
return interval * curStep + smallest
end
function smooth_ship(shipX, shipY, lastTime, nextTime, curTime)
local velocity = ba.createVector(
smooth_value(shipX.velocity[1], shipY.velocity[1], lastTime, nextTime, curTime),
smooth_value(shipX.velocity[2], shipY.velocity[2], lastTime, nextTime, curTime),
smooth_value(shipX.velocity[3], shipY.velocity[3], lastTime, nextTime, curTime)
)
local rotationalVelocity = ba.createVector(
smooth_value(shipX.rotationalVelocity[1], shipY.rotationalVelocity[1], lastTime, nextTime, curTime),
smooth_value(shipX.rotationalVelocity[2], shipY.rotationalVelocity[2], lastTime, nextTime, curTime),
smooth_value(shipX.rotationalVelocity[3], shipY.rotationalVelocity[3], lastTime, nextTime, curTime)
)
play_ship({["name"]=shipY.name,
["velocity"]=velocity,
["rotationalVelocity"]=rotationalVelocity,
["primaryFire"]=shipY.primaryFire,
["target"]=shipY.target,
["targetSubSystem"]=shipY.targetSubSystem,
})
end
function play_ship(shipValue)
local ship = mn.Ships[shipValue.name]
if (ship:isValid()) then
--set physics
ship.Physics.Velocity = shipValue.velocity
ship.Physics.RotationalVelocity = shipValue.rotationalVelocity
--set targets
if (shipValue.target ~= nil) then
local shipTarget = mn.Ships[shipValue.target]
ship.Target = shipTarget
ship.TargetSubSystem = shipTarget[shipValue.targetSubSystem]
else
ship.Target = nil
ship.TargetSubSystem = nil
end
--shoot
if (shipValue.primaryFire == true) then
ship:firePrimary()
--gr.drawString(shipValue.name .. ": PEW")
end
end
end
function play(curTime)
local countEntries = #recordTable
local nextTime
if (playIndex <= countEntries) then
--go to nearest entry
for j=playIndex, countEntries do
if (curTime <= recordTable[j].missionTime) then
playIndex = j
nextTime = recordTable[playIndex].missionTime
break
end
end
if (nextTime == nil) then
return
end
if (curTime == nextTime) then
--values for ships at exact that time existing
local countShips = #recordTable[playIndex].ships
for j=1, countShips do
play_ship(recordTable[playIndex].ships[j])
end
lastTime = curTime
elseif (curTime < nextTime) then
--no values for ships at exact that time, we have to interpolate them
if (playIndex > 1 and recordTable[playIndex-1].missionTime > curTime) then
return
end
local countShips = #recordTable[playIndex].ships
for j=1, countShips do
local newShipValues = recordTable[playIndex].ships[j]
play_ship(newShipValues)
--[[
interpolating is bugged yet =(
--get old ship values for interpolation
local oldShipValues = nil
if (playIndex > 1) then
oldShipValues = recordTable[playIndex-1].ships[j]
else
oldShipValues = {
["velocity"]=ba.createVector(0,0,0),
["rotationalVelocity"]=ba.createVector(0,0,0),
}
end
--smooth_ship(oldShipValues, newShipValues, lastTime, nextTime, curTime)
]]
end
lastTime = curTime
end
playIndex = playIndex + 1
else
recordTable = nil --make it nil so this function will not be called again
end
end
local floatMissionTime = mn.getMissionTime()
if (floatMissionTime ~= nil and recordTable ~= nil) then
local missionTime = round(floatMissionTime)
if (PLAY_MODE == false) then
record(missionTime)
else
play(missionTime)
end
end
]
$Application: FS2_Open
$On Mission End:
[
function booltostring(boolValue)
if (boolValue == true) then
return "1"
else
return "0"
end
end
function save_file(filename, filepath)
local file = cf.openFile(filename, "w", filepath)
--write each entries
local countEntries = #recordTable
for i=1, countEntries do
if (i>1) then
file:write("\n")
end
gr.drawString("REC Saving: Entry " .. i)
file:write(recordTable[i].missionTime .. ":")
--write each ships
local countShips = #recordTable[i].ships
for j=1, countShips do
if (j>1) then
file:write("#")
end
local ship = recordTable[i].ships[j]
file:write(ship.name .. "/")
file:write(ship.velocity[1] .. "," .. ship.velocity[2] .. "," .. ship.velocity[3] .. "/")
file:write(ship.rotationalVelocity[1] .. "," .. ship.rotationalVelocity[2] .. "," .. ship.rotationalVelocity[3] .. "/")
file:write(booltostring(ship.primaryFire) .. "/")
file:write(ship.target)
file:write("/" .. ship.targetSubSystem)
end
end
file:flush()
file:close()
end
function save()
if (RECORD_STATIC_NAME == true) then
save_file("record_1.cfg", RECORD_PATH)
else
local i=1
while true do
local filename = "record_" .. i .. ".cfg"
if (cf.fileExists(filename, RECORD_PATH, false) ~= true) then
save_file(filename, RECORD_PATH)
break
end
i = i + 1
end
end
end
if (recordTable ~= nil) then
if (PLAY_MODE == false) then
--save the record table
save()
end
--kill the arrays
recordTable = nil
end
]
#End
If you want to switch between play and record mode you have to change the value of the boolean variable PLAY_MODE inside the script. Keep in mind that you have to restart FS2 to reload the changed script.
It would be great if you can try it out. Feedback appreciated, thank you.
EDIT:
01/08/10: fixed small bug