-- ==============================================================
-- ORBITER MODULE: VanTV4.lua
-- ==============================================================
-- Copyright 2025 (c) John Frye
-- Licensed under the MIT License
-- ==============================================================
-- load modules
separations = require "Modules.separations"
animations = require "Modules.animations"
-- register custom exhaust texture
local hTex = oapi.register_exhausttexture("Exhaust2")
-- pre load meshes
local mesh_booster = oapi.load_meshglobal("VanTV4/TV4stg1")
local mesh_sustainer = oapi.load_meshglobal("VanTV4/TV4stg2")
local mesh_fairing1 = oapi.load_meshglobal("VanTV4/TV4fairing1")
local mesh_fairing2 = oapi.load_meshglobal("VanTV4/TV4fairing2")
local mesh_third = oapi.load_meshglobal("VanTV4/TV4stg3")
local mesh_arm1 = oapi.load_meshglobal("VanTV4/TV4arm1")
local mesh_arm2 = oapi.load_meshglobal("VanTV4/TV4arm2")
local mesh_arm3 = oapi.load_meshglobal("VanTV4/TV4arm3")
local mesh_arm4 = oapi.load_meshglobal("VanTV4/TV4arm4")
local mesh_sat = oapi.load_meshglobal("VanTV4/TV4sat")
-- SetClassCaps implementation
function clbk_setclasscaps(cfg)
-- physical vessel parameters
vi:set_size(10.948)
vi:set_emptymass(2997)
vi:set_pmi({x=25.23,y=25.23,z=0.16})
vi:set_crosssections({x=20.05,y=20.63,z=1.08})
vi:set_rotdrag({x=0.1,y=0.1,z=0.01})
vi:set_cameraoffset({x=0,y=0,z=10.948})
-- touchdown points
local tdn = {}
tdn[1] = {pos={x=0,y=-2,z=-20},stiffness=1e7,damping=1e5,mu=3.0,mu_lng=3.0}
tdn[2] = {pos={x=-2,y=2,z=-20},stiffness=1e7,damping=1e5,mu=3.0,mu_lng=3.0}
tdn[3] = {pos={x=2,y=2,z=-20},stiffness=1e7,damping=1e5,mu=3.0,mu_lng=3.0}
tdn[4] = {pos={x=0,y=0,z=11},stiffness=1e7,damping=1e5,mu=3.0,mu_lng=3.0}
vi:set_touchdownpoints(tdn)
-- fuel
local hProp = vi:create_propellantresource(7279)
-- main engine
thmain = vi:create_thruster({pos={x=0,y=0,z=-10.948},dir={x=0,y=0,z=1},maxth0=125000,hprop=hProp,isp0=2480})
local thgmain = vi:create_thrustergroup({thmain},THGROUP.MAIN)
-- engine exhaust definitions
vi:add_exhaust(thmain,40,1,{x=0,y=0,z=-10.948},{x=0, y=0, z=-1},hTex)
-- smoke
local exhaust_main = {
flags = 0,
srcsize = 2.0,
srcrate = 13,
v0 = 150.0,
srcspread = 0.1,
lifetime = 0.2,
growthrate = 16,
atmslowdown = 1.0,
ltype = PARTICLE.DIFFUSE,
levelmap = PARTICLE.LVL_PSQRT,
lmin = 0,
lmax = 1,
atmsmap = PARTICLE.ATM_PLOG,
amin = 1e-5,
amax = 0.1,
tex = nil
}
vi:add_exhauststream (thmain, {x=0,y=0,z=-14}, exhaust_main)
-- RCS engines
local th = 700
local isp = 2500
local thrcs0 = vi:create_thruster({pos={x=0,y=1.5,z=-10.5},dir={x=0,y=-1,z=0},maxth0=th,isp0=isp,hprop=hProp})
local thrcs1 = vi:create_thruster({pos={x=0,y=-1.5,z=-10.5},dir={x=0,y=1,z=0},maxth0=th,isp0=isp,hprop=hProp})
local thrcs2 = vi:create_thruster({pos={x=0,y=1.5,z=-10.5},dir={x=-1,y=0,z=0},maxth0=th,isp0=isp,hprop=hProp})
local thrcs3 = vi:create_thruster({pos={x=0,y=-1.5,z=-10.5},dir={x=1,y=0,z=0},maxth0=th,isp0=isp,hprop=hProp})
local thrcs4 = vi:create_thruster({pos={x=0,y=1.5,z=-10.5},dir={x=1,y=0,z=0},maxth0=th,isp0=isp,hprop=hProp})
local thrcs5 = vi:create_thruster({pos={x=0,y=-1.5,z=-10.5},dir={x=-1,y=0,z=0},maxth0=th,isp0=isp,hprop=hProp})
local thrcs6 = vi:create_thruster({pos={x=-1.5,y=0,z=-10.5},dir={x=1,y=0,z=0},maxth0=th,isp0=isp,hprop=hProp})
local thrcs7 = vi:create_thruster({pos={x=1.5,y=0,z=-10.5},dir={x=-1,y=0,z=0},maxth0=th,isp0=isp,hprop=hProp})
vi:create_thrustergroup({thrcs0},THGROUP.ATT_PITCHUP)
vi:create_thrustergroup({thrcs1},THGROUP.ATT_PITCHDOWN)
vi:create_thrustergroup({thrcs2,thrcs3},THGROUP.ATT_BANKLEFT)
vi:create_thrustergroup({thrcs4,thrcs5},THGROUP.ATT_BANKRIGHT)
vi:create_thrustergroup({thrcs6},THGROUP.ATT_YAWLEFT)
vi:create_thrustergroup({thrcs7},THGROUP.ATT_YAWRIGHT)
-- add meshes
mh_Booster = vi:add_mesh(mesh_booster,{x=0,y=0,z=-4.237})
mh_Sustainer = vi:add_mesh(mesh_sustainer,{x=0,y=0,z=5.362})
mh_Fairing1 = vi:add_mesh(mesh_fairing1,{x=0,y=0,z=9.6})
mh_Fairing2 = vi:add_mesh(mesh_fairing2,{x=0,y=0,z=9.6})
-- make meshes only visible from the outside
vi:set_mesh_visibility_mode(mh_Booster,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Sustainer,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Fairing1,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Fairing2,MESHVIS.EXTERNAL)
-- configuration status
config = 0
end
-- Second stage
function sustainer()
--clear previous fuel and thrusters
vi:clear_propellantresources()
vi:clear_thrusters()
-- physical vessel parameters
vi:set_size(4.49)
vi:set_emptymass(890)
vi:set_pmi({x=3.51,y=3.51,z=0.09})
vi:set_crosssections({x=6.51,y=6.58,z=0.74})
vi:set_rotdrag({x=0.1,y=0.1,z=0.01})
vi:set_cameraoffset({x=0,y=0,z=4.49})
-- fuel
local hsProp = vi:create_propellantresource(1296)
-- main engine
local thsmain = vi:create_thruster({pos={x=0,y=0,z=-3.75},dir={x=0,y=0,z=1},maxth0=32600,hprop=hsProp,isp0=2610})
local thsgmain = vi:create_thrustergroup({thsmain},THGROUP.MAIN)
-- engine exhaust definitions
vi:add_exhaust(thsmain,20,1,{x=0,y=0,z=-3.75},{x=0,y=0,z=-1},hTex)
-- smoke
local exhaust_main = {
flags = 0,
srcsize = 2.0,
srcrate = 13,
v0 = 150.0,
srcspread = 0.1,
lifetime = 0.2,
growthrate = 16,
atmslowdown = 1.0,
ltype = PARTICLE.DIFFUSE,
levelmap = PARTICLE.LVL_PSQRT,
lmin = 0,
lmax = 1,
atmsmap = PARTICLE.ATM_PLOG,
amin = 1e-5,
amax = 0.1,
tex = nil
}
vi:add_exhauststream (thsmain, {x=0,y=0,z=-5}, exhaust_main)
-- RCS engines
local sth = 200
local sisp = 2500
local thsrcs0 = vi:create_thruster({pos={x=0,y=1,z=-4.25},dir={x=0,y=-1,z=0},maxth0=sth,isp0=sisp,hprop=hsProp})
local thsrcs1 = vi:create_thruster({pos={x=0,y=-1,z=-4.25},dir={x=0,y=1,z=0},maxth0=sth,isp0=sisp,hprop=hsProp})
local thsrcs2 = vi:create_thruster({pos={x=0,y=1,z=-4.25},dir={x=-1,y=0,z=0},maxth0=sth,isp0=sisp,hprop=hsProp})
local thsrcs3 = vi:create_thruster({pos={x=0,y=-1,z=-4.25},dir={x=1,y=0,z=0},maxth0=sth,isp0=sisp,hprop=hsProp})
local thsrcs4 = vi:create_thruster({pos={x=0,y=1,z=-4.25},dir={x=1,y=0,z=0},maxth0=sth,isp0=sisp,hprop=hsProp})
local thsrcs5 = vi:create_thruster({pos={x=0,y=-1,z=-4.25},dir={x=-1,y=0,z=0},maxth0=sth,isp0=sisp,hprop=hsProp})
local thsrcs6 = vi:create_thruster({pos={x=-1,y=0,z=-4.25},dir={x=1,y=0,z=0},maxth0=sth,isp0=sisp,hprop=hsProp})
local thsrcs7 = vi:create_thruster({pos={x=1,y=0,z=-4.25},dir={x=-1,y=0,z=0},maxth0=sth,isp0=sisp,hprop=hsProp})
vi:create_thrustergroup({thsrcs0},THGROUP.ATT_PITCHUP)
vi:create_thrustergroup({thsrcs1},THGROUP.ATT_PITCHDOWN)
vi:create_thrustergroup({thsrcs2,thsrcs3},THGROUP.ATT_BANKLEFT)
vi:create_thrustergroup({thsrcs4,thsrcs5},THGROUP.ATT_BANKRIGHT)
vi:create_thrustergroup({thsrcs6},THGROUP.ATT_YAWLEFT)
vi:create_thrustergroup({thsrcs7},THGROUP.ATT_YAWRIGHT)
-- clear previous meshes
vi:clear_meshes(true)
-- add meshes
mh_Sustainer = vi:add_mesh(mesh_sustainer,{x=0,y=0,z=5.25})
mh_Fairing1 = vi:add_mesh(mesh_fairing1,{x=0,y=0,z=9.452})
mh_Fairing2 = vi:add_mesh(mesh_fairing2,{x=0,y=0,z=9.452})
mh_Third = vi:add_mesh(mesh_third,{x=0,y=0,z=7.46})
mh_Sat = vi:add_mesh(mesh_sat,{x=0,y=0,z=8.297})
mh_Arm1 = vi:add_mesh(mesh_arm1,{x=0,y=0.21,z=8.115})
mh_Arm2 = vi:add_mesh(mesh_arm2,{x=-0.21,y=0,z=8.115})
mh_Arm3 = vi:add_mesh(mesh_arm3,{x=0,y=-0.21,z=8.115})
mh_Arm4 = vi:add_mesh(mesh_arm4,{x=0.21,y=0,z=8.115})
-- make meshes only visible from the outside
vi:set_mesh_visibility_mode(mh_Sustainer,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Fairing1,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Fairing2,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Third,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Sat,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Arm1,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Arm2,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Arm3,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Arm4,MESHVIS.EXTERNAL)
-- center vessel
vi:shiftCG({x=0.0,y=0.0,z=5.25})
-- configuration status
config = 1
end
-- Third stage
function third()
--clear previous fuel and thrusters
vi:clear_propellantresources()
vi:clear_thrusters()
-- physical vessel parameters
vi:set_size(0.78)
vi:set_emptymass(33)
vi:set_pmi({x=0.14,y=0.14,z=0.02})
vi:set_crosssections({x=0.57,y=0.57,z=0.15})
vi:set_rotdrag({x=0.1,y=0.1,z=0.01})
vi:set_cameraoffset({x=0,y=0,z=2.78})
animations.Trans_Third()
animations.Trans_Sat1()
-- fuel
local hsProp = vi:create_propellantresource(163)
-- main engine
local thsmain = vi:create_thruster({pos={x=0,y=0,z=-0.8},dir={x=0,y=0,z=1},maxth0=10400,hprop=hsProp,isp0=2380})
local thsgmain = vi:create_thrustergroup({thsmain},THGROUP.MAIN)
-- engine exhaust definitions
vi:add_exhaust(thsmain,5,0.5,{x=0,y=0,z=-0.8},{x=0,y=0,z=-1},hTex)
-- smoke
local exhaust_main = {
flags = 0,
srcsize = 2.0,
srcrate = 13,
v0 = 150.0,
srcspread = 0.1,
lifetime = 0.2,
growthrate = 16,
atmslowdown = 1.0,
ltype = PARTICLE.DIFFUSE,
levelmap = PARTICLE.LVL_PSQRT,
lmin = 0,
lmax = 1,
atmsmap = PARTICLE.ATM_PLOG,
amin = 1e-5,
amax = 0.1,
tex = nil
}
vi:add_exhauststream (thsmain, {x=0,y=0,z=-5}, exhaust_main)
-- clear previous meshes
vi:clear_meshes(true)
-- add meshes
mh_Third = vi:add_mesh(mesh_third,{x=0,y=0,z=2.21})
mh_Sat = vi:add_mesh(mesh_sat,{x=0,y=0,z=3.047})
mh_Arm1 = vi:add_mesh(mesh_arm1,{x=0,y=0.21,z=2.865})
mh_Arm2 = vi:add_mesh(mesh_arm2,{x=-0.21,y=0,z=2.865})
mh_Arm3 = vi:add_mesh(mesh_arm3,{x=0,y=-0.21,z=2.865})
mh_Arm4 = vi:add_mesh(mesh_arm4,{x=0.21,y=0,z=2.865})
-- make meshes only visible from the outside
vi:set_mesh_visibility_mode(mh_Third,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Sat,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Arm1,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Arm2,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Arm3,MESHVIS.EXTERNAL)
vi:set_mesh_visibility_mode(mh_Arm4,MESHVIS.EXTERNAL)
-- center vessel
vi:shiftCG({x=0.0,y=0.0,z=2.21})
-- configuration status
config = 2
end
function sat()
--clear previous fuel and thrusters
vi:clear_propellantresources()
vi:clear_thrusters()
-- physical vessel parameters
vi:set_size(0.31)
vi:set_emptymass(1.46)
vi:set_pmi({x=0.01,y=0.02,z=0.02})
vi:set_crosssections({x=0.03,y=0.03,z=0.03})
vi:set_rotdrag({x=0.01,y=0.01,z=0.01})
vi:set_cameraoffset({x=0,y=0,z=0})
animations.Trans_Sat2()
-- clear previous meshes
--vi:clear_meshes(true)
-- add meshes
--mh_Sat = vi:add_mesh(mesh_sat,{x=0,y=0,z=0.975})
-- make meshes only visible from the outside
--vi:set_mesh_visibility_mode(mh_Sat,MESHVIS.EXTERNAL)
vi:del_mesh(0)
vi:del_mesh(2)
vi:del_mesh(3)
vi:del_mesh(4)
vi:del_mesh(5)
-- center vessel
vi:shiftCG({x=0.0,y=0.0,z=0.975})
--vi:set_angvel({x=0,y=0,z=9.75})
config = 3
end
-- Make sure fairings are only released once
local split = false
function fairings()
if config == 1 and not split then
split = true
separations.fairing1_separation()
separations.fairing2_separation()
return
end
end
-- Make sure arms are only released once
local released = false
function arms()
if not released then
released = true
separations.arm1_separation()
separations.arm2_separation()
separations.arm3_separation()
separations.arm4_separation()
return
end
end
-- Configuration with fairings
function fairing()
fairing_config = 0
end
-- Configuration without fairings
function no_fairing()
vi:del_mesh(mh_Fairing1)
vi:del_mesh(mh_Fairing2)
fairing_config = 1
end
-- Configuration with arms
function arm()
arm_config = 0
end
-- Configuration without arms
function no_arm()
vi:del_mesh(mh_Arm1)
vi:del_mesh(mh_Arm2)
vi:del_mesh(mh_Arm3)
vi:del_mesh(mh_Arm4)
arm_config = 1
end
-- Jettison the various stages
function jettison()
if config == 0 then
separations.booster_separation()
elseif config == 1 then
if fairing_config == 1 then
separations.sustainer_separation()
end
end
if config == 2 then
if arm_config == 1 and spin_timer > 9 then
separations.third_separation()
separations.strap_separation()
separations.girth1_separation()
separations.girth2_separation()
end
end
end
-- Spin the third stage and satellite assembly
function third_spin1()
if config == 2 then
local third_status = vi:get_animation(anim_Third)
local sat1_status = vi:get_animation(anim_Sat1)
local third_proc = (third_status + da) % 1
local sat1_proc = (sat1_status + da) % 1
vi:set_animation (anim_Third, third_proc)
if arm_config == 1 then
vi:set_animation (anim_Sat1, sat1_proc)
end
end
end
-- Spin the third stage and satellite assembly
function third_spin()
if spin_timer == nil then
spin_timer = 0
end
local tm = oapi.get_simstep()
spin_timer = spin_timer + tm
if spin_timer >= 10 then
spin_timer = 10
end
if sat1_spin == nil then
sat1_spin = 0
end
if config == 2 then
local third_status = vi:get_animation(anim_Third)
local third_proc = (third_status + da) % 1
vi:set_animation (anim_Third, third_proc)
sat1_spin = (sat1_spin + (spin_timer*(tm/6.75))) % 1
if arm_config == 1 and spin_timer > 2 then
vi:set_animation (anim_Sat1, sat1_spin)
end
end
end
-- Spin the jettisoned satellite
function sat_spin()
if config == 3 then
local sat2_status = vi:get_animation (anim_Sat2)
local sat2_proc = (sat2_status + da2) % 1
vi:set_animation (anim_Sat2, sat2_proc)
end
end
timer = 0
function clbk_prestep(simt,simdt,mjd)
if config == 2 then
da = simdt * 1.5
third_spin()
timer = timer + simdt
if timer < 2 then
return
end
if arm_config ~= 1 then
arms()
end
end
da2 = simdt * 1.5
sat_spin()
end
-- Keystrokes
function clbk_consumebufferedkey(key, down, kstate)
if not down then
return false
end
if oapi.keydown(kstate, OAPI_KEY.J) then
if not vi:playback() then
jettison()
end
end
if oapi.keydown(kstate, OAPI_KEY.F) then
if not vi:playback() then
fairings()
end
end
end
-- Recording playback events
function clbk_playbackevent(simt, event_t, event_type, event)
if event_type == "JET" then
if event == "BOOSTER" then
separations.booster_playback()
return true
elseif event == "THIRD" then
separations.sustainer_playback()
return true
elseif event == "SAT" then
separations.third_playback()
return true
end
end
if event_type == "SPLIT" then
if event == "FAIRINGS" then
fairings()
return true
elseif event == "ARMS" then
arms()
return true
end
end
return false
end
-- Read from a saved scenario
function clbk_loadstateex(scn, vs2)
local match = {}
for line in scenario_lines(scn) do
if scenario_line_match(line, "CONFIGURATION %d", match) then
config = match.res[1]
elseif scenario_line_match(line, "FAIRINGS %d", match) then
fairing_config = match.res[1]
elseif scenario_line_match(line, "ARMS %d", match) then
arm_config = match.res[1]
else
vi:parse_scenario_line_ex(line, vs2) -- unrecognised option - pass to Orbiter's generic parser
end
end
if config == 1 then
sustainer()
elseif config == 2 then
third()
elseif config == 3 then
sat()
end
if fairing_config == 0 then
fairing()
elseif fairing_config == 1 then
no_fairing()
end
if arm_config == 0 then
arm()
elseif arm_config == 1 then
if config ~= 3 then
no_arm()
end
end
end
-- Write a saved scenario
function clbk_savestate(scn)
oapi.writescenario_string(scn, "CONFIGURATION", tostring(config))
oapi.writescenario_string(scn, "FAIRINGS", tostring(fairing_config))
oapi.writescenario_string(scn, "ARMS", tostring(arm_config))
end