bastd.game.meteorshower
Defines a bomb-dodging mini-game.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines a bomb-dodging mini-game.""" 4 5# ba_meta require api 7 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10import random 11from typing import TYPE_CHECKING 12 13import ba 14from bastd.actor.bomb import Bomb 15from bastd.actor.onscreentimer import OnScreenTimer 16 17if TYPE_CHECKING: 18 from typing import Any, Sequence 19 20 21class Player(ba.Player['Team']): 22 """Our player type for this game.""" 23 24 def __init__(self) -> None: 25 super().__init__() 26 self.death_time: float | None = None 27 28 29class Team(ba.Team[Player]): 30 """Our team type for this game.""" 31 32 33# ba_meta export game 34class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): 35 """Minigame involving dodging falling bombs.""" 36 37 name = 'Meteor Shower' 38 description = 'Dodge the falling bombs.' 39 available_settings = [ba.BoolSetting('Epic Mode', default=False)] 40 scoreconfig = ba.ScoreConfig( 41 label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B' 42 ) 43 44 # Print messages when players die (since its meaningful in this game). 45 announce_player_deaths = True 46 47 # Don't allow joining after we start 48 # (would enable leave/rejoin tomfoolery). 49 allow_mid_activity_joins = False 50 51 # We're currently hard-coded for one map. 52 @classmethod 53 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 54 return ['Rampage'] 55 56 # We support teams, free-for-all, and co-op sessions. 57 @classmethod 58 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 59 return ( 60 issubclass(sessiontype, ba.DualTeamSession) 61 or issubclass(sessiontype, ba.FreeForAllSession) 62 or issubclass(sessiontype, ba.CoopSession) 63 ) 64 65 def __init__(self, settings: dict): 66 super().__init__(settings) 67 68 self._epic_mode = settings.get('Epic Mode', False) 69 self._last_player_death_time: float | None = None 70 self._meteor_time = 2.0 71 self._timer: OnScreenTimer | None = None 72 73 # Some base class overrides: 74 self.default_music = ( 75 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL 76 ) 77 if self._epic_mode: 78 self.slow_motion = True 79 80 def on_begin(self) -> None: 81 super().on_begin() 82 83 # Drop a wave every few seconds.. and every so often drop the time 84 # between waves ..lets have things increase faster if we have fewer 85 # players. 86 delay = 5.0 if len(self.players) > 2 else 2.5 87 if self._epic_mode: 88 delay *= 0.25 89 ba.timer(delay, self._decrement_meteor_time, repeat=True) 90 91 # Kick off the first wave in a few seconds. 92 delay = 3.0 93 if self._epic_mode: 94 delay *= 0.25 95 ba.timer(delay, self._set_meteor_timer) 96 97 self._timer = OnScreenTimer() 98 self._timer.start() 99 100 # Check for immediate end (if we've only got 1 player, etc). 101 ba.timer(5.0, self._check_end_game) 102 103 def on_player_leave(self, player: Player) -> None: 104 # Augment default behavior. 105 super().on_player_leave(player) 106 107 # A departing player may trigger game-over. 108 self._check_end_game() 109 110 # overriding the default character spawning.. 111 def spawn_player(self, player: Player) -> ba.Actor: 112 spaz = self.spawn_player_spaz(player) 113 114 # Let's reconnect this player's controls to this 115 # spaz but *without* the ability to attack or pick stuff up. 116 spaz.connect_controls_to_player( 117 enable_punch=False, enable_bomb=False, enable_pickup=False 118 ) 119 120 # Also lets have them make some noise when they die. 121 spaz.play_big_death_sound = True 122 return spaz 123 124 # Various high-level game events come through this method. 125 def handlemessage(self, msg: Any) -> Any: 126 if isinstance(msg, ba.PlayerDiedMessage): 127 128 # Augment standard behavior. 129 super().handlemessage(msg) 130 131 curtime = ba.time() 132 133 # Record the player's moment of death. 134 # assert isinstance(msg.spaz.player 135 msg.getplayer(Player).death_time = curtime 136 137 # In co-op mode, end the game the instant everyone dies 138 # (more accurate looking). 139 # In teams/ffa, allow a one-second fudge-factor so we can 140 # get more draws if players die basically at the same time. 141 if isinstance(self.session, ba.CoopSession): 142 # Teams will still show up if we check now.. check in 143 # the next cycle. 144 ba.pushcall(self._check_end_game) 145 146 # Also record this for a final setting of the clock. 147 self._last_player_death_time = curtime 148 else: 149 ba.timer(1.0, self._check_end_game) 150 151 else: 152 # Default handler: 153 return super().handlemessage(msg) 154 return None 155 156 def _check_end_game(self) -> None: 157 living_team_count = 0 158 for team in self.teams: 159 for player in team.players: 160 if player.is_alive(): 161 living_team_count += 1 162 break 163 164 # In co-op, we go till everyone is dead.. otherwise we go 165 # until one team remains. 166 if isinstance(self.session, ba.CoopSession): 167 if living_team_count <= 0: 168 self.end_game() 169 else: 170 if living_team_count <= 1: 171 self.end_game() 172 173 def _set_meteor_timer(self) -> None: 174 ba.timer( 175 (1.0 + 0.2 * random.random()) * self._meteor_time, 176 self._drop_bomb_cluster, 177 ) 178 179 def _drop_bomb_cluster(self) -> None: 180 181 # Random note: code like this is a handy way to plot out extents 182 # and debug things. 183 loc_test = False 184 if loc_test: 185 ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) 186 ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) 187 ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) 188 ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) 189 190 # Drop several bombs in series. 191 delay = 0.0 192 for _i in range(random.randrange(1, 3)): 193 # Drop them somewhere within our bounds with velocity pointing 194 # toward the opposite side. 195 pos = ( 196 -7.3 + 15.3 * random.random(), 197 11, 198 -5.57 + 2.1 * random.random(), 199 ) 200 dropdir = -1.0 if pos[0] > 0 else 1.0 201 vel = ( 202 (-5.0 + random.random() * 30.0) * dropdir, 203 random.uniform(-3.066, -4.12), 204 0, 205 ) 206 ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) 207 delay += 0.1 208 self._set_meteor_timer() 209 210 def _drop_bomb( 211 self, position: Sequence[float], velocity: Sequence[float] 212 ) -> None: 213 Bomb(position=position, velocity=velocity).autoretain() 214 215 def _decrement_meteor_time(self) -> None: 216 self._meteor_time = max(0.01, self._meteor_time * 0.9) 217 218 def end_game(self) -> None: 219 cur_time = ba.time() 220 assert self._timer is not None 221 start_time = self._timer.getstarttime() 222 223 # Mark death-time as now for any still-living players 224 # and award players points for how long they lasted. 225 # (these per-player scores are only meaningful in team-games) 226 for team in self.teams: 227 for player in team.players: 228 survived = False 229 230 # Throw an extra fudge factor in so teams that 231 # didn't die come out ahead of teams that did. 232 if player.death_time is None: 233 survived = True 234 player.death_time = cur_time + 1 235 236 # Award a per-player score depending on how many seconds 237 # they lasted (per-player scores only affect teams mode; 238 # everywhere else just looks at the per-team score). 239 score = int(player.death_time - self._timer.getstarttime()) 240 if survived: 241 score += 50 # A bit extra for survivors. 242 self.stats.player_scored(player, score, screenmessage=False) 243 244 # Stop updating our time text, and set the final time to match 245 # exactly when our last guy died. 246 self._timer.stop(endtime=self._last_player_death_time) 247 248 # Ok now calc game results: set a score for each team and then tell 249 # the game to end. 250 results = ba.GameResults() 251 252 # Remember that 'free-for-all' mode is simply a special form 253 # of 'teams' mode where each player gets their own team, so we can 254 # just always deal in teams and have all cases covered. 255 for team in self.teams: 256 257 # Set the team score to the max time survived by any player on 258 # that team. 259 longest_life = 0.0 260 for player in team.players: 261 assert player.death_time is not None 262 longest_life = max(longest_life, player.death_time - start_time) 263 264 # Submit the score value in milliseconds. 265 results.set_team_score(team, int(1000.0 * longest_life)) 266 267 self.end(results=results)
22class Player(ba.Player['Team']): 23 """Our player type for this game.""" 24 25 def __init__(self) -> None: 26 super().__init__() 27 self.death_time: float | None = None
Our player type for this game.
Inherited Members
- ba._player.Player
- actor
- on_expire
- team
- customdata
- sessionplayer
- node
- position
- exists
- getname
- is_alive
- get_icon
- assigninput
- resetinput
Our team type for this game.
Inherited Members
- ba._team.Team
- manual_init
- customdata
- on_expire
- sessionteam
35class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): 36 """Minigame involving dodging falling bombs.""" 37 38 name = 'Meteor Shower' 39 description = 'Dodge the falling bombs.' 40 available_settings = [ba.BoolSetting('Epic Mode', default=False)] 41 scoreconfig = ba.ScoreConfig( 42 label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B' 43 ) 44 45 # Print messages when players die (since its meaningful in this game). 46 announce_player_deaths = True 47 48 # Don't allow joining after we start 49 # (would enable leave/rejoin tomfoolery). 50 allow_mid_activity_joins = False 51 52 # We're currently hard-coded for one map. 53 @classmethod 54 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 55 return ['Rampage'] 56 57 # We support teams, free-for-all, and co-op sessions. 58 @classmethod 59 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 60 return ( 61 issubclass(sessiontype, ba.DualTeamSession) 62 or issubclass(sessiontype, ba.FreeForAllSession) 63 or issubclass(sessiontype, ba.CoopSession) 64 ) 65 66 def __init__(self, settings: dict): 67 super().__init__(settings) 68 69 self._epic_mode = settings.get('Epic Mode', False) 70 self._last_player_death_time: float | None = None 71 self._meteor_time = 2.0 72 self._timer: OnScreenTimer | None = None 73 74 # Some base class overrides: 75 self.default_music = ( 76 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL 77 ) 78 if self._epic_mode: 79 self.slow_motion = True 80 81 def on_begin(self) -> None: 82 super().on_begin() 83 84 # Drop a wave every few seconds.. and every so often drop the time 85 # between waves ..lets have things increase faster if we have fewer 86 # players. 87 delay = 5.0 if len(self.players) > 2 else 2.5 88 if self._epic_mode: 89 delay *= 0.25 90 ba.timer(delay, self._decrement_meteor_time, repeat=True) 91 92 # Kick off the first wave in a few seconds. 93 delay = 3.0 94 if self._epic_mode: 95 delay *= 0.25 96 ba.timer(delay, self._set_meteor_timer) 97 98 self._timer = OnScreenTimer() 99 self._timer.start() 100 101 # Check for immediate end (if we've only got 1 player, etc). 102 ba.timer(5.0, self._check_end_game) 103 104 def on_player_leave(self, player: Player) -> None: 105 # Augment default behavior. 106 super().on_player_leave(player) 107 108 # A departing player may trigger game-over. 109 self._check_end_game() 110 111 # overriding the default character spawning.. 112 def spawn_player(self, player: Player) -> ba.Actor: 113 spaz = self.spawn_player_spaz(player) 114 115 # Let's reconnect this player's controls to this 116 # spaz but *without* the ability to attack or pick stuff up. 117 spaz.connect_controls_to_player( 118 enable_punch=False, enable_bomb=False, enable_pickup=False 119 ) 120 121 # Also lets have them make some noise when they die. 122 spaz.play_big_death_sound = True 123 return spaz 124 125 # Various high-level game events come through this method. 126 def handlemessage(self, msg: Any) -> Any: 127 if isinstance(msg, ba.PlayerDiedMessage): 128 129 # Augment standard behavior. 130 super().handlemessage(msg) 131 132 curtime = ba.time() 133 134 # Record the player's moment of death. 135 # assert isinstance(msg.spaz.player 136 msg.getplayer(Player).death_time = curtime 137 138 # In co-op mode, end the game the instant everyone dies 139 # (more accurate looking). 140 # In teams/ffa, allow a one-second fudge-factor so we can 141 # get more draws if players die basically at the same time. 142 if isinstance(self.session, ba.CoopSession): 143 # Teams will still show up if we check now.. check in 144 # the next cycle. 145 ba.pushcall(self._check_end_game) 146 147 # Also record this for a final setting of the clock. 148 self._last_player_death_time = curtime 149 else: 150 ba.timer(1.0, self._check_end_game) 151 152 else: 153 # Default handler: 154 return super().handlemessage(msg) 155 return None 156 157 def _check_end_game(self) -> None: 158 living_team_count = 0 159 for team in self.teams: 160 for player in team.players: 161 if player.is_alive(): 162 living_team_count += 1 163 break 164 165 # In co-op, we go till everyone is dead.. otherwise we go 166 # until one team remains. 167 if isinstance(self.session, ba.CoopSession): 168 if living_team_count <= 0: 169 self.end_game() 170 else: 171 if living_team_count <= 1: 172 self.end_game() 173 174 def _set_meteor_timer(self) -> None: 175 ba.timer( 176 (1.0 + 0.2 * random.random()) * self._meteor_time, 177 self._drop_bomb_cluster, 178 ) 179 180 def _drop_bomb_cluster(self) -> None: 181 182 # Random note: code like this is a handy way to plot out extents 183 # and debug things. 184 loc_test = False 185 if loc_test: 186 ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) 187 ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) 188 ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) 189 ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) 190 191 # Drop several bombs in series. 192 delay = 0.0 193 for _i in range(random.randrange(1, 3)): 194 # Drop them somewhere within our bounds with velocity pointing 195 # toward the opposite side. 196 pos = ( 197 -7.3 + 15.3 * random.random(), 198 11, 199 -5.57 + 2.1 * random.random(), 200 ) 201 dropdir = -1.0 if pos[0] > 0 else 1.0 202 vel = ( 203 (-5.0 + random.random() * 30.0) * dropdir, 204 random.uniform(-3.066, -4.12), 205 0, 206 ) 207 ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) 208 delay += 0.1 209 self._set_meteor_timer() 210 211 def _drop_bomb( 212 self, position: Sequence[float], velocity: Sequence[float] 213 ) -> None: 214 Bomb(position=position, velocity=velocity).autoretain() 215 216 def _decrement_meteor_time(self) -> None: 217 self._meteor_time = max(0.01, self._meteor_time * 0.9) 218 219 def end_game(self) -> None: 220 cur_time = ba.time() 221 assert self._timer is not None 222 start_time = self._timer.getstarttime() 223 224 # Mark death-time as now for any still-living players 225 # and award players points for how long they lasted. 226 # (these per-player scores are only meaningful in team-games) 227 for team in self.teams: 228 for player in team.players: 229 survived = False 230 231 # Throw an extra fudge factor in so teams that 232 # didn't die come out ahead of teams that did. 233 if player.death_time is None: 234 survived = True 235 player.death_time = cur_time + 1 236 237 # Award a per-player score depending on how many seconds 238 # they lasted (per-player scores only affect teams mode; 239 # everywhere else just looks at the per-team score). 240 score = int(player.death_time - self._timer.getstarttime()) 241 if survived: 242 score += 50 # A bit extra for survivors. 243 self.stats.player_scored(player, score, screenmessage=False) 244 245 # Stop updating our time text, and set the final time to match 246 # exactly when our last guy died. 247 self._timer.stop(endtime=self._last_player_death_time) 248 249 # Ok now calc game results: set a score for each team and then tell 250 # the game to end. 251 results = ba.GameResults() 252 253 # Remember that 'free-for-all' mode is simply a special form 254 # of 'teams' mode where each player gets their own team, so we can 255 # just always deal in teams and have all cases covered. 256 for team in self.teams: 257 258 # Set the team score to the max time survived by any player on 259 # that team. 260 longest_life = 0.0 261 for player in team.players: 262 assert player.death_time is not None 263 longest_life = max(longest_life, player.death_time - start_time) 264 265 # Submit the score value in milliseconds. 266 results.set_team_score(team, int(1000.0 * longest_life)) 267 268 self.end(results=results)
Minigame involving dodging falling bombs.
66 def __init__(self, settings: dict): 67 super().__init__(settings) 68 69 self._epic_mode = settings.get('Epic Mode', False) 70 self._last_player_death_time: float | None = None 71 self._meteor_time = 2.0 72 self._timer: OnScreenTimer | None = None 73 74 # Some base class overrides: 75 self.default_music = ( 76 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL 77 ) 78 if self._epic_mode: 79 self.slow_motion = True
Instantiate the Activity.
Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.
Whether players should be allowed to join in the middle of this activity. Note that Sessions may not allow mid-activity-joins even if the activity says its ok.
53 @classmethod 54 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 55 return ['Rampage']
Called by the default ba.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given ba.Session type.
58 @classmethod 59 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 60 return ( 61 issubclass(sessiontype, ba.DualTeamSession) 62 or issubclass(sessiontype, ba.FreeForAllSession) 63 or issubclass(sessiontype, ba.CoopSession) 64 )
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
81 def on_begin(self) -> None: 82 super().on_begin() 83 84 # Drop a wave every few seconds.. and every so often drop the time 85 # between waves ..lets have things increase faster if we have fewer 86 # players. 87 delay = 5.0 if len(self.players) > 2 else 2.5 88 if self._epic_mode: 89 delay *= 0.25 90 ba.timer(delay, self._decrement_meteor_time, repeat=True) 91 92 # Kick off the first wave in a few seconds. 93 delay = 3.0 94 if self._epic_mode: 95 delay *= 0.25 96 ba.timer(delay, self._set_meteor_timer) 97 98 self._timer = OnScreenTimer() 99 self._timer.start() 100 101 # Check for immediate end (if we've only got 1 player, etc). 102 ba.timer(5.0, self._check_end_game)
Called once the previous ba.Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
104 def on_player_leave(self, player: Player) -> None: 105 # Augment default behavior. 106 super().on_player_leave(player) 107 108 # A departing player may trigger game-over. 109 self._check_end_game()
Called when a ba.Player is leaving the Activity.
112 def spawn_player(self, player: Player) -> ba.Actor: 113 spaz = self.spawn_player_spaz(player) 114 115 # Let's reconnect this player's controls to this 116 # spaz but *without* the ability to attack or pick stuff up. 117 spaz.connect_controls_to_player( 118 enable_punch=False, enable_bomb=False, enable_pickup=False 119 ) 120 121 # Also lets have them make some noise when they die. 122 spaz.play_big_death_sound = True 123 return spaz
Spawn something for the provided ba.Player.
The default implementation simply calls spawn_player_spaz().
126 def handlemessage(self, msg: Any) -> Any: 127 if isinstance(msg, ba.PlayerDiedMessage): 128 129 # Augment standard behavior. 130 super().handlemessage(msg) 131 132 curtime = ba.time() 133 134 # Record the player's moment of death. 135 # assert isinstance(msg.spaz.player 136 msg.getplayer(Player).death_time = curtime 137 138 # In co-op mode, end the game the instant everyone dies 139 # (more accurate looking). 140 # In teams/ffa, allow a one-second fudge-factor so we can 141 # get more draws if players die basically at the same time. 142 if isinstance(self.session, ba.CoopSession): 143 # Teams will still show up if we check now.. check in 144 # the next cycle. 145 ba.pushcall(self._check_end_game) 146 147 # Also record this for a final setting of the clock. 148 self._last_player_death_time = curtime 149 else: 150 ba.timer(1.0, self._check_end_game) 151 152 else: 153 # Default handler: 154 return super().handlemessage(msg) 155 return None
General message handling; can be passed any message object.
219 def end_game(self) -> None: 220 cur_time = ba.time() 221 assert self._timer is not None 222 start_time = self._timer.getstarttime() 223 224 # Mark death-time as now for any still-living players 225 # and award players points for how long they lasted. 226 # (these per-player scores are only meaningful in team-games) 227 for team in self.teams: 228 for player in team.players: 229 survived = False 230 231 # Throw an extra fudge factor in so teams that 232 # didn't die come out ahead of teams that did. 233 if player.death_time is None: 234 survived = True 235 player.death_time = cur_time + 1 236 237 # Award a per-player score depending on how many seconds 238 # they lasted (per-player scores only affect teams mode; 239 # everywhere else just looks at the per-team score). 240 score = int(player.death_time - self._timer.getstarttime()) 241 if survived: 242 score += 50 # A bit extra for survivors. 243 self.stats.player_scored(player, score, screenmessage=False) 244 245 # Stop updating our time text, and set the final time to match 246 # exactly when our last guy died. 247 self._timer.stop(endtime=self._last_player_death_time) 248 249 # Ok now calc game results: set a score for each team and then tell 250 # the game to end. 251 results = ba.GameResults() 252 253 # Remember that 'free-for-all' mode is simply a special form 254 # of 'teams' mode where each player gets their own team, so we can 255 # just always deal in teams and have all cases covered. 256 for team in self.teams: 257 258 # Set the team score to the max time survived by any player on 259 # that team. 260 longest_life = 0.0 261 for player in team.players: 262 assert player.death_time is not None 263 longest_life = max(longest_life, player.death_time - start_time) 264 265 # Submit the score value in milliseconds. 266 results.set_team_score(team, int(1000.0 * longest_life)) 267 268 self.end(results=results)
Tell the game to wrap up and call ba.Activity.end() immediately.
This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (ba.GameActivity.setup_standard_time_limit()) will work with the game.
Inherited Members
- ba._teamgame.TeamGameActivity
- on_transition_in
- spawn_player_spaz
- end
- ba._gameactivity.GameActivity
- allow_pausing
- allow_kick_idle_players
- create_settings_ui
- getscoreconfig
- getname
- get_display_string
- get_team_display_string
- get_description
- get_description_display_string
- get_available_settings
- get_settings_display_string
- map
- get_instance_display_string
- get_instance_scoreboard_display_string
- get_instance_description
- get_instance_description_short
- on_continue
- is_waiting_for_continue
- continue_or_end_game
- on_player_join
- respawn_player
- spawn_player_if_exists
- setup_standard_powerup_drops
- setup_standard_time_limit
- show_zoom_message
- ba._activity.Activity
- settings_raw
- teams
- players
- is_joining_activity
- use_fixed_vr_overlay
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- transition_time
- can_show_ad_on_death
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_team_join
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- create_team
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps