def _update_obj(self, entity: Entity, state: PhysicsState, origin: Entity) -> None: # update planet objects self._obj.pos = entity.screen_pos(origin) / self._scale_factor 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) / self._scale_factor
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 _create_obj(self, entity: Entity, origin: Entity, texture: Optional[str]) -> vpython.compound: """Creates the habitat, and also a new minimap scene and habitat.""" assert texture is not None habitat = self._create_hab(entity, texture) habitat.pos = entity.screen_pos(origin) main_scene = vpython.canvas.get_selected() self._minimap_canvas = vpython.canvas(width=200, height=150, userspin=False, userzoom=False, up=common.DEFAULT_UP, forward=common.DEFAULT_FORWARD) self._minimap_canvas.append_to_caption( # The element styling will be removed at runtime. This just hides # this helptext during startup. "<span class='helptext' style='display: none'>" "This small 'minimap' shows the Habitat's orientation. The red " "arrow represents the Hab's velocity relative to the reference, " "and the gray arrow points to position of the reference." "</span>") self._small_habitat = self._create_hab(entity, texture) self._ref_arrow = vpython.arrow(color=vpython.color.gray(0.5)) self._velocity_arrow = vpython.arrow(color=vpython.color.red) main_scene.select() self._broken: bool = False return habitat
def _create_obj(self, entity: Entity, origin: Entity, texture: Optional[str]) -> vpython.sphere: assert texture is not None earth = self._create_earth(entity, texture) earth.pos = entity.screen_pos(origin) earth.clouds.pos = earth.pos return earth
def _label_text(self, entity: Entity) -> str: label = entity.name if entity.broken: label += ' [BROKEN]' label += '\nFuel: ' + common.format_num(entity.fuel, " kg") if entity.landed_on == AYSE: label += '\nDocked' elif entity.landed(): label += '\nLanded' return label
def _label_text(self, entity: Entity, state: PhysicsState) -> str: label = super()._label_text(entity, state) if entity.broken: label += ' [BROKEN]' label += '\nFuel: ' + common.format_num(entity.fuel, " kg") if entity.landed_on == common.AYSE: label += '\nDocked' elif entity.landed(): label += '\nLanded' return label
def _create_obj(self, entity: Entity, origin: Entity, texture: Optional[str]) -> vpython.sphere: return vpython.sphere(pos=entity.screen_pos(origin), axis=calc.angle_to_vpy(entity.heading), up=DEFAULT_UP, forward=DEFAULT_FORWARD, radius=entity.r, make_trail=True, retain=10000, texture=texture, bumpmap=vpython.bumpmaps.gravel, shininess=Planet.SHININIESS)
def _create_obj(self, entity: Entity, origin: Entity, texture: Optional[str]) -> vpython.sphere: return vpython.sphere( pos=entity.screen_pos(origin), axis=calc.angle_to_vpy(entity.heading), up=vpython.vector(0, 0, 1), # So the planet doesn't intersect the landing graphic radius=entity.r * 0.9, make_trail=True, retain=10000, texture=texture, bumpmap=vpython.bumpmaps.gravel, shininess=Planet.SHININIESS)
def _create_obj(self, entity: Entity, origin: Entity, texture: Optional[str]) -> vpython.sphere: main_body = vpython.box() side_panels = vpython.box(height=2, width=0.5, length=0.6) obj = vpython.compound([main_body, side_panels], make_trail=True, texture=texture, bumpmap=vpython.textures.gravel) obj.pos = entity.screen_pos(origin) obj.axis = calc.angle_to_vpy(entity.heading) obj.length = entity.r * 2 obj.height = entity.r * 2 obj.width = entity.r * 2 # A compound object doesn't actually have a radius, but we need to # monkey-patch this for when we recentre the camera, to determine the # relevant_range of the space station obj.radius = entity.r return obj
def test_fields(self): def test_field(pe: Entity, field: str, val): pe.proto.Clear() setattr(pe, field, val) self.assertEqual(getattr(pe.proto, field), val) pe = Entity(protos.Entity()) test_field(pe, 'name', 'test') test_field(pe, 'x', 5) test_field(pe, 'y', 5) test_field(pe, 'vx', 5) test_field(pe, 'vy', 5) test_field(pe, 'r', 5) test_field(pe, 'mass', 5) test_field(pe, 'heading', 5) test_field(pe, 'spin', 5) test_field(pe, 'fuel', 5) test_field(pe, 'throttle', 5) test_field(pe, 'landed_on', 'other_test') test_field(pe, 'broken', True) test_field(pe, 'artificial', True)
def _create_obj(self, entity: Entity, origin: Entity, texture_path: Optional[str]) -> vpython.sphere: ship = vpython.cone(pos=vpython.vector(5, 0, 0), axis=vpython.vector(5, 0, 0), radius=3) entrance = vpython.extrusion( path=[vpython.vec(0, 0, 0), vpython.vec(4, 0, 0)], shape=[ vpython.shapes.circle(radius=3), vpython.shapes.rectangle(pos=[0, 0], width=0.5, height=0.5) ], pos=vpython.vec(3, 0, 0)) docking_arm = vpython.extrusion( path=[ vpython.vec(0, 0, 0), vpython.vec(1.5, 0, 0), vpython.vec(1.5, 0.5, 0) ], shape=[vpython.shapes.circle(radius=0.03)]) obj = vpython.compound([ship, entrance, docking_arm], make_trail=True, texture=vpython.textures.metal, bumpmap=vpython.bumpmaps.gravel) obj.pos = entity.screen_pos(origin) obj.axis = calc.angle_to_vpy(entity.heading) obj.length = entity.r * 2 obj.height = entity.r * 2 obj.width = entity.r * 2 # A compound object doesn't actually have a radius, but we need to # monkey-patch this for when we recentre the camera, to determine the # relevant_range of the space station obj.radius = entity.r return obj
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 _update_obj(self, entity: Entity, state: PhysicsState, origin: Entity): super()._update_obj(entity, state, origin) self._obj.boosters.pos = self._obj.pos self._obj.boosters.axis = self._obj.axis # Attach the parachute to the forward cone of the habitat. self._obj.parachute.pos = ( self._obj.pos + calc.angle_to_vpy(entity.heading) * entity.r * 0.8) parachute_is_visible = ((state.craft == entity.name) and state.parachute_deployed) if parachute_is_visible: drag = calc.drag(state) drag_mag = np.inner(drag, drag) for parachute in [self._obj.parachute, self._small_habitat.parachute]: if not parachute_is_visible or drag_mag < 0.00001: # If parachute_is_visible == False, don't show the parachute. # If the drag is basically zero, don't show the parachute. parachute.visible = False continue if drag_mag > 0.1: parachute.width = self.PARACHUTE_RADIUS * 2 parachute.height = self.PARACHUTE_RADIUS * 2 else: # Below a certain threshold the parachute deflates. parachute.width = self.PARACHUTE_RADIUS parachute.height = self.PARACHUTE_RADIUS parachute.axis = -vpython.vec(*drag, 0) parachute.visible = True if not self._broken and entity.broken: # We weren't broken before, but looking at new data we realize # we're now broken. Change the habitat texture. new_texture = self._obj.texture.replace('Habitat.jpg', 'Habitat-broken.jpg') assert new_texture != self._obj.texture, \ f'{new_texture!r} == {self._obj.texture!r}' self._obj.texture = new_texture self._small_habitat.texture = new_texture self._broken = entity.broken elif self._broken and not entity.broken: # We were broken before, but we've repaired ourselves somehow. new_texture = self._obj.texture.replace('Habitat-broken.jpg', 'Habitat.jpg') assert new_texture != self._obj.texture, \ f'{new_texture!r} == {self._obj.texture!r}' self._obj.texture = new_texture self._small_habitat.texture = new_texture self._broken = entity.broken # Set reference and target arrows of the minimap habitat. same = state.reference == entity.name default = vpython.vector(0, 0, -1) ref_arrow_axis = (entity.screen_pos(state.reference_entity()).norm() * entity.r * -1.2) v = entity.v - state.reference_entity().v velocity_arrow_axis = \ vpython.vector(*v, 0).norm() * entity.r self._ref_arrow.axis = default if same else ref_arrow_axis self._velocity_arrow.axis = default if same else velocity_arrow_axis self._small_habitat.axis = self._obj.axis self._small_habitat.boosters.axis = self._obj.axis # Invisible-ize the SRBs if we ran out. if state.srb_time == common.SRB_EMPTY and self._obj.boosters.visible: self._obj.boosters.visible = False self._small_habitat.boosters.visible = False
def _label_text(self, entity: Entity, state: PhysicsState) -> str: label = super()._label_text(entity, state) label += '\nFuel: ' + common.format_num(entity.fuel, " kg") if entity.landed(): label += '\nLanded' return label
def _label_text(self, entity: Entity) -> str: return ( f'{entity.name}\n' f'Fuel: {common.format_num(entity.fuel, " kg")}' + ('\nLanded' if entity.landed() else '') )
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