Hard Light Productions Forums
Modding, Mission Design, and Coding => The Scripting Workshop => Topic started by: stfx on January 05, 2010, 01:08:32 pm
-
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
-
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.
-
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
-
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? =)
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 =)
-
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...
-
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
-
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 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 :)
-
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.
-
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. :)