def _update_obj(self, entity: Entity, state: PhysicsState, origin: Entity) -> None: # update planet objects self._obj.pos = entity.screen_pos(origin) self._obj.axis = calc.angle_to_vpy(entity.heading) # update label objects self._label.text = self._label_text(entity) self._label.pos = entity.screen_pos(origin) # update landing graphic objects self._update_landing_graphic(self._small_landing_graphic, entity, state.craft_entity()) self._update_landing_graphic(self._large_landing_graphic, entity, state.craft_entity())
def _reconcile_entity_dynamics(y: PhysicsState) -> PhysicsState: """Idempotent helper that sets velocities and spins of some entities. This is in its own function because it has a couple calling points.""" # Navmode auto-rotation if y.navmode != Navmode['Manual']: craft = y.craft_entity() craft.spin = calc.navmode_spin(y) # Keep landed entities glued together landed_on = y.LandedOn for index in landed_on: # If we're landed on something, make sure we move in lockstep. lander = y[index] ground = y[landed_on[index]] if ground.name == AYSE and lander.name == HABITAT: # Always put the Habitat at the docking port. lander.pos = ( ground.pos - calc.heading_vector(ground.heading) * (lander.r + ground.r)) else: norm = lander.pos - ground.pos unit_norm = norm / calc.fastnorm(norm) lander.pos = ground.pos + unit_norm * (ground.r + lander.r) lander.spin = ground.spin lander.v = calc.rotational_speed(lander, ground) return y
def __call__(self, t, y_1d) -> float: """Return 0 when the craft is landed but thrusting enough to lift off, and a positive value otherwise.""" y = PhysicsState(y_1d, self.initial_state._proto_state) if y.craft is None: # There is no craft, return early. return np.inf craft = y.craft_entity() if not craft.landed(): # We don't have to lift off because we already lifted off. return np.inf planet = y[craft.landed_on] if planet.artificial: # If we're docked with another satellite, undocking is governed # by other mechanisms. Ignore this. return np.inf thrust = common.craft_capabilities[craft.name].thrust * craft.throttle if y.srb_time > 0 and y.craft == HABITAT: thrust += common.SRB_THRUST pos = craft.pos - planet.pos # This is the G(m1*m2)/r^2 formula weight = common.G * craft.mass * planet.mass / np.inner(pos, pos) # This should be positive when the craft isn't thrusting enough, and # zero when it is thrusting enough. return max(0, common.LAUNCH_TWR - thrust / weight)
def relevant_atmosphere(flight_state: PhysicsState) \ -> Optional[Entity]: """Returns the closest entity that has an atmosphere, or None if there are no such entities close enough to effect the craft.""" craft = flight_state.craft_entity() closest_atmosphere: Optional[Entity] = None closest_distance = np.inf atmosphere_indices = flight_state.Atmospheres if not atmosphere_indices: # There are no entities with atmospheres return None for index in atmosphere_indices: atmosphere = flight_state[index] dist = fastnorm(atmosphere.pos - craft.pos) # np.inner is the same as the magnitude squared # https://stackoverflow.com/a/35213951 # We use the squared distance because it's much faster to calculate. if dist < closest_distance: # We found a closer planet with an atmosphere exponential = (-(dist - craft.r - atmosphere.r) / 1000 / atmosphere.atmosphere_scaling) if exponential > -20: # Entity has an atmosphere and it's close enough to be relevant closest_distance = dist closest_atmosphere = atmosphere return closest_atmosphere
def rotation_formatter(state: PhysicsState) -> str: deg_spin = round(np.degrees(state.craft_entity().spin), ndigits=1) if deg_spin < 0: return f"{-deg_spin} °/s cw" elif deg_spin > 0: return f"{deg_spin} °/s ccw" else: return f"{deg_spin} °/s"
def engine_acceleration(state: PhysicsState) -> float: """Acceleration due to engine thrust.""" craft = state.craft_entity() if craft.name == common.HABITAT and state.srb_time > 0: srb_thrust = common.SRB_THRUST else: srb_thrust = 0 return (common.craft_capabilities[craft.name].thrust * craft.throttle + srb_thrust) / (craft.mass + craft.fuel)
def update(self, state: PhysicsState, origin: Entity): if not self._visible: self._hyperbola.visible = False self._ring.visible = False return orb_params = calc.orbit_parameters(state.reference_entity(), state.craft_entity()) thickness = min( self.PROJECTION_THICKNESS * vpython.canvas.get_selected().range, abs(0.1 * max(orb_params.major_axis, orb_params.minor_axis))) pos = orb_params.centre - origin.pos direction = vpython.vector(*orb_params.eccentricity, 0) e_mag = calc.fastnorm(orb_params.eccentricity) if e_mag < 1: # Project an elliptical orbit self._hyperbola.visible = False self._ring.pos = vpython.vector(*pos, 0) # size.x is actually the thickness, confusingly. # size.y and size.z are the width and length of the ellipse. self._ring.size.y = orb_params.major_axis self._ring.size.z = orb_params.minor_axis self._ring.up = direction self._ring.size.x = thickness self._ring.visible = True elif e_mag > 1: # Project a hyperbolic orbit self._ring.visible = False self._hyperbola.origin = vpython.vector(*pos, 0) # For a vpython.curve object, setting the axis is confusing. # To control direction, set the .up attribute (magnitude doesn't # matter for .up) # To control size, set the .size attribute (magnitude matters). # Setting .axis will also change .size, not sure how. self._hyperbola.size = vpython.vector(orb_params.minor_axis / 2, orb_params.major_axis / 2, 1) self._hyperbola.up = -direction self._hyperbola.radius = thickness self._hyperbola.visible = True assert self._hyperbola.axis.z == 0, self._hyperbola.axis assert self._hyperbola.up.z == 0, self._hyperbola.up else: # e == 1 pretty much never happens, and if it does we'll just wait # until it goes away return
def navmode_heading(flight_state: PhysicsState) -> float: """ Returns the heading that the craft should be facing in current navmode. Don't call this with Manual navmode. If the navmode is relative to the reference, and the reference is set to the craft, this will return the craft's current heading as a sane default. """ navmode = flight_state.navmode craft = flight_state.craft_entity() ref = flight_state.reference_entity() targ = flight_state.target_entity() requested_vector: np.ndarray if navmode == Navmode['Manual']: raise ValueError('Autopilot requested for manual navmode') elif navmode.name == 'CCW Prograde': if flight_state.reference == flight_state.craft: return craft.heading normal = craft.pos - ref.pos requested_vector = np.array([-normal[1], normal[0]]) elif navmode == Navmode['CW Retrograde']: if flight_state.reference == flight_state.craft: return craft.heading normal = craft.pos - ref.pos requested_vector = np.array([normal[1], -normal[0]]) elif navmode == Navmode['Depart Reference']: if flight_state.reference == flight_state.craft: return craft.heading requested_vector = craft.pos - ref.pos elif navmode == Navmode['Approach Target']: if flight_state.target == flight_state.craft: return craft.heading requested_vector = targ.pos - craft.pos elif navmode == Navmode['Pro Targ Velocity']: if flight_state.target == flight_state.craft: return craft.heading requested_vector = craft.v - targ.v elif navmode == Navmode['Anti Targ Velocity']: if flight_state.target == flight_state.craft: return craft.heading requested_vector = targ.v - craft.v else: raise ValueError(f'Got an unexpected NAVMODE: {flight_state.navmode}') return np.arctan2(requested_vector[1], requested_vector[0])
def navmode_spin(flight_state: PhysicsState) -> float: """Returns a spin that will orient the craft according to the navmode.""" craft = flight_state.craft_entity() requested_heading = navmode_heading(flight_state) ccw_distance = (requested_heading - craft.heading) % np.radians(360) cw_distance = (craft.heading - requested_heading) % np.radians(360) if ccw_distance < cw_distance: heading_difference = ccw_distance else: heading_difference = -cw_distance if abs(heading_difference) < common.AUTOPILOT_FINE_CONTROL_RADIUS: # The unit analysis doesn't work out here I'm sorry. Basically, # the closer we are to the target heading, the slower we adjust. return heading_difference else: # We can spin at most common.AUTOPILOT_SPEED return np.sign(heading_difference) * common.AUTOPILOT_SPEED
def drag(flight_state: PhysicsState) -> np.ndarray: """Calculates the directional drag exerted on the craft by an atmosphere. """ atmosphere = relevant_atmosphere(flight_state) if atmosphere is None: return np.array([0, 0]) craft = flight_state.craft_entity() air_v = rotational_speed(craft, atmosphere) wind = craft.v - air_v if np.inner(wind, wind) < 0.01: # The craft is stationary return np.array([0, 0]) wind_angle = np.arctan2(wind[1], wind[0]) # I know I've said this about other pieces of code, but I really have no # idea how this code works. I just lifted it from OrbitV because I couldn't # find simple atmospheric drag models. # Also sorry, unit analysis doesn't work inside this function. # TODO: how does this work outside of the atmosphere? # TODO: disabled while I figure out if this only works during telemetry i_know_what_this_aoa_code_does = False if i_know_what_this_aoa_code_does: aoa = wind_angle - pitch(craft, atmosphere) aoa = np.sign(np.sign(np.cos(aoa)) - 1) aoa = aoa**3 if aoa > 0.5: aoa = 1 - aoa aoa_vector = -abs(aoa) * np.array([ np.sin(wind_angle + (np.pi / 2) * np.sign(aoa)), np.cos(wind_angle + (np.pi / 2) * np.sign(aoa)) ]) return aoa_vector drag_profile = common.HAB_DRAG_PROFILE if flight_state.parachute_deployed: drag_profile += common.PARACHUTE_DRAG_PROFILE drag_acc = \ pressure(craft, atmosphere) * fastnorm(wind) ** 2 * drag_profile return drag_acc * (wind / fastnorm(wind))
def _one_request(request: Request, y0: PhysicsState) \ -> PhysicsState: """Interface to set habitat controls. Use an argument to change habitat throttle or spinning, and simulation will restart with this new information.""" log.info(f'At simtime={y0.timestamp}, ' f'Got command {MessageToString(request, as_one_line=True)}') if request.ident != Request.TIME_ACC_SET: # Reveal the type of y0.craft as str (not None). assert y0.craft is not None if request.ident == Request.HAB_SPIN_CHANGE: if y0.navmode != Navmode['Manual']: # We're in autopilot, ignore this command return y0 craft = y0.craft_entity() if not craft.landed(): craft.spin += request.spin_change elif request.ident == Request.HAB_THROTTLE_CHANGE: y0.craft_entity().throttle += request.throttle_change elif request.ident == Request.HAB_THROTTLE_SET: y0.craft_entity().throttle = request.throttle_set elif request.ident == Request.TIME_ACC_SET: assert request.time_acc_set >= 0 y0.time_acc = request.time_acc_set elif request.ident == Request.ENGINEERING_UPDATE: # Multiply this value by 100, because OrbitV considers engines at # 100% to be 100x the maximum thrust. common.craft_capabilities[HABITAT] = \ common.craft_capabilities[HABITAT]._replace( thrust=100 * request.engineering_update.max_thrust) hab = y0[HABITAT] ayse = y0[AYSE] hab.fuel = request.engineering_update.hab_fuel ayse.fuel = request.engineering_update.ayse_fuel y0[HABITAT] = hab y0[AYSE] = ayse if request.engineering_update.module_state == \ Request.DETACHED_MODULE and \ MODULE not in y0._entity_names and \ not hab.landed(): # If the Habitat is freely floating and engineering asks us to # detach the Module, spawn in the Module. module = Entity(protos.Entity( name=MODULE, mass=100, r=10, artificial=True)) module.pos = (hab.pos - (module.r + hab.r) * calc.heading_vector(hab.heading)) module.v = calc.rotational_speed(module, hab) y0_proto = y0.as_proto() y0_proto.entities.extend([module.proto]) y0 = PhysicsState(None, y0_proto) elif request.ident == Request.UNDOCK: habitat = y0[HABITAT] if habitat.landed_on == AYSE: ayse = y0[AYSE] habitat.landed_on = '' norm = habitat.pos - ayse.pos unit_norm = norm / calc.fastnorm(norm) habitat.v += unit_norm * common.UNDOCK_PUSH habitat.spin = ayse.spin y0[HABITAT] = habitat elif request.ident == Request.REFERENCE_UPDATE: y0.reference = request.reference elif request.ident == Request.TARGET_UPDATE: y0.target = request.target elif request.ident == Request.LOAD_SAVEFILE: y0 = common.load_savefile(common.savefile(request.loadfile)) elif request.ident == Request.NAVMODE_SET: y0.navmode = Navmode(request.navmode) if y0.navmode == Navmode['Manual']: y0.craft_entity().spin = 0 elif request.ident == Request.PARACHUTE: y0.parachute_deployed = request.deploy_parachute elif request.ident == Request.IGNITE_SRBS: if round(y0.srb_time) == common.SRB_FULL: y0.srb_time = common.SRB_BURNTIME elif request.ident == Request.TOGGLE_COMPONENT: component = y0.engineering.components[request.component_to_toggle] component.connected = not component.connected elif request.ident == Request.TOGGLE_RADIATOR: radiator = y0.engineering.radiators[request.radiator_to_toggle] radiator.functioning = not radiator.functioning elif request.ident == Request.CONNECT_RADIATOR_TO_LOOP: radiator = y0.engineering.radiators[request.radiator_to_loop.rad] radiator.attached_to_coolant_loop = request.radiator_to_loop.loop elif request.ident == Request.TOGGLE_COMPONENT_COOLANT: component = y0.engineering.components[request.component_to_loop.component] # See comments on _component_coolant_cnxn_transitions for more info. component.coolant_connection = ( _component_coolant_cnxn_transitions [request.component_to_loop.loop] [component.coolant_connection] ) return y0
def _run_simulation(self, t: float, y: PhysicsState) -> None: # An overview of how time is managed: # # self._last_simtime is the main thread's latest idea of # what the current time is in the simulation. Every call to # get_state(), self._timetime_of_last_request is incremented by the # amount of time that passed since the last call to get_state(), # factoring in time_acc # # self._solutions is a fixed-size queue of ODE solutions. # Each element has an attribute, t_max, which describes the largest # time that the solution can be evaluated at and still be accurate. # The highest such t_max should always be larger than the current # simulation time, i.e. self._last_simtime proto_state = y._proto_state while not self._stopping_simthread: derive_func = functools.partial( self._derive, pass_through_state=proto_state) events: List[Event] = [ CollisionEvent(y, self.R), HabFuelEvent(y), LiftoffEvent(y), SrbFuelEvent(), HabReactorTempEvent(), AyseReactorTempEvent() ] if y.craft is not None: events.append(HighAccEvent( derive_func, self._artificials, TIME_ACC_TO_BOUND[round(y.time_acc)], y.time_acc, len(y))) ivp_out = scipy.integrate.solve_ivp( fun=derive_func, t_span=[t, t + min(y.time_acc, 10 * self.MAX_STEP_SIZE)], # solve_ivp requires a 1D y0 array y0=y.y0(), events=events, dense_output=True, max_step=self.MAX_STEP_SIZE ) if not ivp_out.success: # Integration error raise Exception(ivp_out.message) # When we create a new solution, let other people know. with self._solutions_cond: # If adding another solution to our max-sized deque would drop # our oldest solution, and the main thread is still asking for # state in the t interval of our oldest solution, take a break # until the main thread has caught up. self._solutions_cond.wait_for( lambda: len(self._solutions) < SOLUTION_CACHE_SIZE or self._last_simtime > self._solutions[0].t_max or self._stopping_simthread ) if self._stopping_simthread: break # self._solutions contains ODE solutions for the interval # [self._solutions[0].t_min, self._solutions[-1].t_max]. self._solutions.append(ivp_out.sol) self._solutions_cond.notify_all() y = PhysicsState(ivp_out.y[:, -1], proto_state) t = ivp_out.t[-1] if ivp_out.status > 0: log.info(f'Got event: {ivp_out.t_events} at t={t}.') for index, event_t in enumerate(ivp_out.t_events): if len(event_t) == 0: # If this event didn't occur, then event_t == [] continue event = events[index] if isinstance(event, CollisionEvent): # Collision, simulation ended. Handled it and continue. assert len(ivp_out.t_events[0]) == 1 assert len(ivp_out.t) >= 2 y = _collision_decision(t, y, events[0]) y = _reconcile_entity_dynamics(y) if isinstance(event, HabFuelEvent): # Something ran out of fuel. for artificial_index in self._artificials: artificial = y[artificial_index] if round(artificial.fuel) != 0: continue log.info(f'{artificial.name} ran out of fuel.') # This craft is out of fuel, the next iteration # won't consume any fuel. Set throttle to zero. artificial.throttle = 0 # Set fuel to a negative value, so it doesn't # trigger the event function. artificial.fuel = 0 if isinstance(event, LiftoffEvent): # A craft has a TWR > 1 craft = y.craft_entity() log.info( 'We have liftoff of the ' f'{craft.name} from {craft.landed_on} at {t}.') craft.landed_on = '' if isinstance(event, SrbFuelEvent): # SRB fuel exhaustion. log.info('SRB exhausted.') y.srb_time = common.SRB_EMPTY if isinstance(event, HighAccEvent): # The acceleration acting on the craft is high, might # result in inaccurate results. SLOOWWWW DOWWWWNNNN. slower_time_acc_index = list( TIME_ACC_TO_BOUND.keys() ).index(round(y.time_acc)) - 1 assert slower_time_acc_index >= 0 slower_time_acc = \ common.TIME_ACCS[slower_time_acc_index] assert slower_time_acc.value > 0 log.info( f'{y.time_acc} is too fast, ' f'slowing down to {slower_time_acc.value}') # We should lower the time acc. y.time_acc = slower_time_acc.value raise PhysicsEngine.RestartSimulationException(t, y) if isinstance(event, HabReactorTempEvent): y.engineering.hab_reactor_alarm = not y.engineering.hab_reactor_alarm if isinstance(event, AyseReactorTempEvent): y.engineering.ayse_reactor_alarm = not y.engineering.ayse_reactor_alarm
def clone_orbitv_state(rnd_path: Path) -> PhysicsState: """Gathers information from an OrbitV savefile, in the *.RND format, as well as a STARSr file. Returns a PhysicsState representing everything we can read.""" proto_state = protos.PhysicalState() starsr_path = rnd_path.parent / 'STARSr' log.info(f"Reading OrbitV file {rnd_path}, " f"using info from adjacent STARSr file {starsr_path}") hab_index: Optional[int] = None ayse_index: Optional[int] = None with open(starsr_path, 'r') as starsr_file: starsr = csv.reader(starsr_file, delimiter=',') background_star_line = next(starsr) while len(background_star_line) == 3: background_star_line = next(starsr) # After the last while loop, the value of background_star_line is # actually a gravity_pair line (a pair of indices, which we also don't # care about). gravity_pair_line = background_star_line while len(gravity_pair_line) == 2: gravity_pair_line = next(starsr) entity_constants_line = gravity_pair_line while len(entity_constants_line) == 6: proto_entity = proto_state.entities.add() # Cast all the elements on a line to floats. # Some strings will be the form '1.234D+04', convert these too. (colour, mass, radius, atmosphere_thickness, atmosphere_scaling, atmosphere_height) \ = map(_string_to_float, entity_constants_line) mass = max(1, mass) proto_entity.mass = mass proto_entity.r = radius if atmosphere_thickness and atmosphere_scaling: # Only set atmosphere fields if they both have a value. proto_entity.atmosphere_thickness = atmosphere_thickness proto_entity.atmosphere_scaling = atmosphere_scaling entity_constants_line = next(starsr) # We skip a line here. It's the timestamp line, but the .RND file will # have more up-to-date timestamp info. entity_positions_line = next(starsr) # We don't care about entity positions, we'll get this from the .RND # file as well. while len(entity_positions_line) == 6: entity_positions_line = next(starsr) entity_name_line = entity_positions_line entity_index = 0 while len(entity_name_line) == 1: name = entity_name_line[0].strip() proto_state.entities[entity_index].name = name if name == common.HABITAT: hab_index = entity_index elif name == common.AYSE: ayse_index = entity_index entity_index += 1 entity_name_line = next(starsr) assert proto_state.entities[0].name == common.SUN, ( 'We assume that the Sun is the first OrbitV entity, this has ' 'implications for how we populate entity positions.') assert hab_index is not None, \ "We didn't see the Habitat in the STARSr file!" assert ayse_index is not None, \ "We didn't see AYSE in the STARSr file!" hab = proto_state.entities[hab_index] ayse = proto_state.entities[ayse_index] with open(rnd_path, 'rb') as rnd: check_byte = rnd.read(1) rnd.read(len("ORBIT5S ")) craft_throttle = _read_single(rnd) / 100 # I don't know what Vflag and Aflag do. _read_int(rnd), _read_int(rnd) navmode = Navmode(SFLAG_TO_NAVMODE[_read_int(rnd)]) if navmode is not None: proto_state.navmode = navmode.value _read_double(rnd) # This is drag, we calculate this ourselves. _read_double(rnd) # This is zoom, we ignore this as well. hab.heading = _read_single(rnd) # Skip centre, ref, and targ information. _read_int(rnd), _read_int(rnd), _read_int(rnd) # We don't track if trails are set. _read_int(rnd) drag_profile = _read_single(rnd) proto_state.parachute_deployed = (drag_profile > 0.002) current_srb_output = _read_single(rnd) if current_srb_output != 0: proto_state.srb_time = common.SRB_BURNTIME # We don't care about colour trails or how times are displayed. _read_int(rnd), _read_int(rnd) _read_double(rnd), _read_double(rnd) # No time acc info either. _read_single(rnd) # Ignore some RCPS info _read_int(rnd) # Eflag? Something that gassimv.bas sets. year = _read_int(rnd) day = _read_int(rnd) hour = _read_int(rnd) minute = _read_int(rnd) second = _read_double(rnd) timestamp = datetime( year, 1, 1, hour=hour, minute=minute, second=int(second)) + timedelta(day - 1) proto_state.timestamp = timestamp.timestamp() ayse.heading = _read_single(rnd) _read_int(rnd) # AYSEscrape seems to do nothing. # TODO: Once we implement wind, fill in these fields. _read_single(rnd), _read_single(rnd) hab.spin = numpy.radians(_read_single(rnd)) docked_constant = _read_int(rnd) if docked_constant != 0: hab.landed_on = common.AYSE _read_single(rnd) # Rad shields, overwritten by enghabv.bas. # TODO: once module is implemented, have it be launched here. _read_int(rnd) # module_state = MODFLAG_TO_MODSTATE[modflag] _read_single(rnd), _read_single(rnd) # AYSEdist and OCESSdist. hab.broken = bool(_read_int(rnd)) ayse.broken = bool(_read_int(rnd)) # TODO: Once we implement nav malfunctions, set this field. _read_single(rnd) # Max thrust, ignored because we'll read it from ORBITSSE.rnd _read_single(rnd) # TODO: Once we implement nav malfunctions, set this field. # Also, apparently this is the same field as two fields ago? _read_single(rnd) # TODO: Once we implement wind, fill in these fields. _read_long(rnd) # TODO: There is some information required from MST.rnd to implement # this field (LONGtarg). _read_single(rnd) # We calculate pressure and agrav. _read_single(rnd), _read_single(rnd) # Note, we skip the first entity, the Sun, since OrbitV does. for i in range(1, min(39, len(proto_state.entities))): entity = proto_state.entities[i] entity.x, entity.y, entity.vx, entity.vy = (_read_double(rnd), _read_double(rnd), _read_double(rnd), _read_double(rnd)) hab.fuel = _read_single(rnd) ayse.fuel = _read_single(rnd) check_byte_2 = rnd.read(1) if check_byte != check_byte_2: log.warning('RND file had inconsistent check bytes: ' f'{check_byte} != {check_byte_2}') # Delete any entity with a (0, 0) position that isn't the Sun. # TODO: We also delete the OCESS launch tower, but once we implement # OCESS we should no longer do this. entity_index = 0 while entity_index < len(proto_state.entities): proto_entity = proto_state.entities[entity_index] if round(proto_entity.x) == 0 and round(proto_entity.y) == 0 and \ proto_entity.name != common.SUN or \ proto_entity.name == common.OCESS: del proto_state.entities[entity_index] else: entity_index += 1 hab.artificial = True ayse.artificial = True # Habitat and AYSE masses are hardcoded in OrbitV. hab.mass = 275000 ayse.mass = 20000000 orbitx_state = PhysicsState(None, proto_state) orbitx_state[common.HABITAT] = Entity(hab) orbitx_state[common.AYSE] = Entity(ayse) craft = orbitx_state.craft_entity() craft.throttle = craft_throttle log.debug(f'Interpreted {rnd_path} and {starsr_path} into this state:') log.debug(repr(orbitx_state)) orbitx_state = _separate_landed_entities(orbitx_state) return orbitx_state
def _write_state_to_osbackup(orbitx_state: PhysicsState, osbackup_path: Path, orbitv_names: List[str]): """Writes information to OSbackup.RND. orbitv_names should be the result of entity_list_from_starsr. Currently only writes information relevant to engineering, but if we want to communicate piloting state to other legacy orbit programs we can write other kinds of information. I found out what information I should be writing here by reading the source for engineering (enghabv.bas) and seeing what information it reads from OSbackup.RND""" assert orbitx_state.craft hab = orbitx_state[common.HABITAT] ayse = orbitx_state[common.AYSE] craft = orbitx_state.craft_entity() max_thrust = common.craft_capabilities[craft.name].thrust timestamp = datetime.fromtimestamp(orbitx_state.timestamp) check_byte = random.choice(string.ascii_letters).encode('ascii') target = (orbitx_state.target if orbitx_state.target in orbitv_names else "AYSE") reference = (orbitx_state.reference if orbitx_state.reference in orbitv_names else "Earth") atmosphere = calc.relevant_atmosphere(orbitx_state) if atmosphere is None: pressure = 0.0 else: pressure = calc.pressure(craft, atmosphere) craft_thrust = (max_thrust * craft.throttle * calc.heading_vector(craft.heading)) craft_drag = calc.drag(orbitx_state) artificial_gravity = numpy.linalg.norm(craft_thrust - craft_drag) with open(osbackup_path, 'r+b') as osbackup: # We replicate the logic of orbit5v.bas:1182 (look for label 800) # in the below code. OSBACKUP.RND is a binary, fixed-width field # file. orbit5v.bas writes to it every second, and it's created in # the same way as any OrbitV save file. # If you're reading the orbit5v.bas source code, note that arrays # and strings in QB are 1-indexed not 0-indexed. osbackup.write(check_byte) osbackup.write("ORBIT5S ".encode('ascii')) # OrbitX keeps throttle as a float, where 100% = 1.0 # OrbitV expects 100% = 100.0 _write_single(100 * max(hab.throttle, ayse.throttle), osbackup) # This is Vflag and Aflag, dunno what these are. _write_int(0, osbackup) _write_int(0, osbackup) _write_int(NAVMODE_TO_SFLAG[orbitx_state.navmode], osbackup) _write_double(numpy.linalg.norm(calc.drag(orbitx_state)), osbackup) _write_double(25, osbackup) # A sane value for OrbitV zoom. # TODO: check if this is off by 90 degrees or something. _write_single(hab.heading, osbackup) _write_int(orbitv_names.index("Habitat"), osbackup) # Centre _write_int(orbitv_names.index(target), osbackup) _write_int(orbitv_names.index(reference), osbackup) _write_int(0, osbackup) # Set trails to off. _write_single(0.006 if orbitx_state.parachute_deployed else 0.002, osbackup) _write_single( (common.SRB_THRUST / 100) if orbitx_state.srb_time > 0 else 0, osbackup) _write_int(0, osbackup) # No colour-coded trails. # Valid values of this are 0, 1, 2. # A value of 1 gets OrbitV to display EST timestamps. _write_int(1, osbackup) _write_double(0.125, osbackup) # Set timestep to the slowest. _write_double(0.125, osbackup) # This is OLDts, also slowest value _write_single(0, osbackup) # Something to do with RSCP, zero it. _write_int(0, osbackup) # Eflag? Something that gassimv.bas sets. _write_int(timestamp.year, osbackup) # QB uses day-of-the-year instead of day-of-the-month. _write_int(int(timestamp.strftime('%j')), osbackup) _write_int(timestamp.hour, osbackup) _write_int(timestamp.minute, osbackup) _write_double(timestamp.second, osbackup) _write_single(ayse.heading, osbackup) _write_int(0, osbackup) # AYSEscrape seemingly does nothing. # TODO: Once we implement wind, fill in these fields. _write_single(0, osbackup) _write_single(0, osbackup) _write_single(numpy.degrees(hab.spin), osbackup) _write_int(150 if hab.landed_on == common.AYSE else 0, osbackup) _write_single(0, osbackup) # Rad shields, written by enghabv.bas. _write_int( MODSTATE_TO_MODFLAG[network.Request.DETACHED_MODULE] if common.MODULE in orbitx_state._entity_names else MODSTATE_TO_MODFLAG[network.Request.NO_MODULE], osbackup) # These next two variables are technically AYSEdist and OCESSdist # but they're used for engineering to determine fueld loading from # OCESS or AYSE. Also, if you can dock with AYSE. So we just tell # engineering if we're landed, but don't tell it any more deets. _write_single(150 if hab.landed_on == common.AYSE else 1000, osbackup) _write_single(150 if hab.landed_on == common.EARTH else 1000, osbackup) _write_int(hab.broken, osbackup) # I think an explosion flag. _write_int(ayse.broken, osbackup) # Same as above. # TODO: Once we implement nav malfunctions, set this field. _write_single(0, osbackup) _write_single(max_thrust, osbackup) # TODO: Once we implement nav malfunctions, set this field. # Also, apparently this is the same field as two fields ago? _write_single(0, osbackup) # TODO: Once we implement wind, fill in these fields. _write_long(0, osbackup) # TODO: Some information required from MST.rnd needed to implement. # this field (LONGtarg). _write_single(0, osbackup) _write_single(pressure, osbackup) _write_single(artificial_gravity, osbackup) for i in range(0, 39): try: entity = orbitx_state[orbitv_names[i]] x, y = entity.pos vx, vy = entity.v except ValueError: x = y = vx = vy = 0 _write_double(x, osbackup) _write_double(y, osbackup) _write_double(vx, osbackup) _write_double(vy, osbackup) _write_single(hab.fuel, osbackup) _write_single(ayse.fuel, osbackup) osbackup.write(check_byte)