Author Topic: Replay recorder script  (Read 3022 times)

0 Members and 1 Guest are viewing this topic.

Offline stfx

  • 25
Replay recorder script
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:
Code: [Select]
#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
« Last Edit: January 08, 2010, 01:28:22 pm by stfx »

 

Offline Wanderer

  • Wiki Warrior
  • 211
  • Mostly harmless
Re: Replay recorder script
Hmm... Try with the 'firePrimary' to use weapons both with and without "stream" flag. If that helps in either case then i probably can help to resolve at least that issue.

And i can take a look at the 'fire secondary' and 'fire countermeasure' as well.
Do not meddle in the affairs of coders for they are soggy and hard to light

 

Offline Thaeris

  • Can take his lumps
  • 211
  • Away in Limbo
Re: Replay recorder script
Hmmm...

If you could create an X-Wing type mission recorder, that would be superb!

Basically, this recorder would let you view the mission from the perspective of any ship from its arrival to its exit/destrution. You could speed up the mission progress rate or slow it down, pause, etc. Just like a video player, really.

Furthermore, if you could get the sim to passively pass information to a new file created once the recording was engaged so as to not slow down/minimally slow down FSO, oh man... Basically, the mission you played could be passed to a format which could be read by a player in the tech room (then there's the problem of expanding the interface... ugh...) and then even exported to a generally useful file format like .ogg. Perhaps the mission recorder files could just be posted in the cutscenes/movies option in the tech room? If this could be made possible, you'd never need to use Fraps with FSO again.  ;7
"trolls are clearly social rejects and therefore should be isolated from society, or perhaps impaled."

-Nuke



"Look on the bright side, how many release dates have been given for Doomsday, and it still isn't out yet.

It's the Duke Nukem Forever of prophecies..."


"Jesus saves.

Everyone else takes normal damage.
"

-Flipside

"pirating software is a lesser evil than stealing but its still evil. but since i pride myself for being evil, almost anything is fair game."


"i never understood why women get the creeps so ****ing easily. i mean most serial killers act perfectly normal, until they kill you."


-Nuke

 

Offline stfx

  • 25
Re: Replay recorder script
Hmm... Try with the 'firePrimary' to use weapons both with and without "stream" flag. If that helps in either case then i probably can help to resolve at least that issue.

And i can take a look at the 'fire secondary' and 'fire countermeasure' as well.

Hey Wanderer thank you for your help. I just checked and 'firePrimary' seems to work only without the "stream" flag. But hey thats good news, isnt it? =)

Quote
If you could create an X-Wing type mission recorder, that would be superb!

Basically, this recorder would let you view the mission from the perspective of any ship from its arrival to its exit/destrution. You could speed up the mission progress rate or slow it down, pause, etc. Just like a video player, really.

Furthermore, if you could get the sim to passively pass information to a new file created once the recording was engaged so as to not slow down/minimally slow down FSO, oh man... Basically, the mission you played could be passed to a format which could be read by a player in the tech room (then there's the problem of expanding the interface... ugh...) and then even exported to a generally useful file format like .ogg. Perhaps the mission recorder files could just be posted in the cutscenes/movies option in the tech room? If this could be made possible, you'd never need to use Fraps with FSO again.

Hehe yeah the possible features would be awesome. I am hoping that someday some programmer implements the code in C++. I just dont see a way to create an .avi or .ogg just with lua. Well it could be possible but there are definately already free libraries aviable to do that in C++ which would also, as you already said, performance wise be the best.

None the less it will work for me and probably other movie makers if Wanderer is able to fix the issues. Thanks again =)

 

Offline Aardwolf

  • 211
  • Posts: 16,384
    • Minecraft
Re: Replay recorder script
Wow... I was just talking about something like this on IRC (but aimed at recording stuff for camera movement)...

I may need to check this out when I have the time for it.

Edit: I haven't tried it yet, but I imagine this might have a bit of trouble recreating beam firings, especially with the randomness of slash beams and such...
« Last Edit: January 05, 2010, 11:51:25 pm by Aardwolf »

 

Offline Wanderer

  • Wiki Warrior
  • 211
  • Mostly harmless
Re: Replay recorder script
Hmm... Try with the 'firePrimary' to use weapons both with and without "stream" flag. If that helps in either case then i probably can help to resolve at least that issue.

And i can take a look at the 'fire secondary' and 'fire countermeasure' as well.

Hey Wanderer thank you for your help. I just checked and 'firePrimary' seems to work only without the "stream" flag. But hey thats good news, isnt it? =)
Yeah, cause now i have pretty good idea of what was wrong in the code
Do not meddle in the affairs of coders for they are soggy and hard to light

 

Offline Nuke

  • Ka-Boom!
  • 212
  • Mutants Worship Me
Re: Replay recorder script
i wrote the PrimaryTriggerDown() function, i used it mainly to check if the player was trying to fire, i think i did that so i could fire a turret when the primary weapons were being fired (though you could probibly use it for a multitude of animation functions). i wanted to do one for escondaries but i couldnt figure out how the secondary code worked.

anyway, the way i would do this is scan for any weapons who's lifeleft is somewhere near its life (i think on a weapons first frame lifeleft == life). then store its position and orientation. then during playback you can re-create them with createWeapon(). this should work for primaries, dumb fire secondaries, and countermeasures. homing weapons would be more complicated, youd have to log their position and orientation each frame, then move the missile each frame during playback. you would probibly also have to set its hitpoints to zero when its supposed to explode.

I can no longer sit back and allow communist infiltration, communist indoctrination, communist subversion, and the international communist conspiracy to sap and impurify all of our precious bodily fluids.

Nuke's Scripting SVN

 

Offline stfx

  • 25
Re: Replay recorder script
i wrote the PrimaryTriggerDown() function, i used it mainly to check if the player was trying to fire, i think i did that so i could fire a turret when the primary weapons were being fired (though you could probibly use it for a multitude of animation functions). i wanted to do one for escondaries but i couldnt figure out how the secondary code worked.

anyway, the way i would do this is scan for any weapons who's lifeleft is somewhere near its life (i think on a weapons first frame lifeleft == life). then store its position and orientation. then during playback you can re-create them with createWeapon(). this should work for primaries, dumb fire secondaries, and countermeasures. homing weapons would be more complicated, youd have to log their position and orientation each frame, then move the missile each frame during playback. you would probibly also have to set its hitpoints to zero when its supposed to explode.

Damn that means I cant use the PrimaryTriggerDown() function. I will try the weapon life thing. That would
also work for beam weapons then, right?

Well we know the target of the homing weapon. I can just send them flying from the starting position to the specific target if I can set the weapons.HomingObject.

None the less thank you for that valuable information :)

 

Offline Aardwolf

  • 211
  • Posts: 16,384
    • Minecraft
Re: Replay recorder script
Probably better to just keep a list of object signatures (using object:getSignature() IIRC), and if it encounters a weapon not on that list, then do whatever it was you were going to do... or so I would think.

 

Offline Mobius

  • Back where he started
  • 213
  • Porto l'azzurro Dolce Stil Novo nella fantascienza
    • Skype
    • Twitter
    • The Lightblue Ribbon | Cultural Project
Re: Replay recorder script
This one seems to have a lot of potential! :eek:

Does anyone mind telling me how to use it properly? I'm working on something which may be significantly boosted by this script. :)
The Lightblue Ribbon

Inferno: Nostos - Alliance
Series Resurrecta: {{FS Wiki Portal}} -  Gehenna's Gate - The Spirit of Ptah - Serendipity (WIP) - <REDACTED> (WIP)
FreeSpace Campaign Restoration Project
A tribute to FreeSpace in my book: Riflessioni dall'Infinito
My interviews: [ 1 ] - [ 2 ] - [ 3 ]