From: Mattia Basaglia Date: Sun, 9 Apr 2017 08:31:41 +0000 (+0100) Subject: Hacks to spawn bots as non-clients X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=541541b8cf7b351c32a1c507b39fa5aa907590f2;p=xonotic%2Fxonotic-data.pk3dir.git Hacks to spawn bots as non-clients --- diff --git a/qcsrc/server/_all.qh b/qcsrc/server/_all.qh index 1fabe4d0f..32617a688 100644 --- a/qcsrc/server/_all.qh +++ b/qcsrc/server/_all.qh @@ -12,7 +12,7 @@ const string STR_OBSERVER = "observer"; #define IS_CLIENT(v) (v.flags & FL_CLIENT) /** want: (IS_CLIENT(v) && !IS_REAL_CLIENT(v)) */ -#define IS_BOT_CLIENT(v) (clienttype(v) == CLIENTTYPE_BOT) +#define IS_BOT_CLIENT(v) (clienttype(v) == CLIENTTYPE_BOT || IS_FAKE_CLIENT(v)) #define IS_FAKE_CLIENT(v) (clienttype(v) == CLIENTTYPE_NOTACLIENT) #define IS_REAL_CLIENT(v) (clienttype(v) == CLIENTTYPE_REAL) /** was: (clienttype(v) == CLIENTTYPE_NOTACLIENT) */ diff --git a/qcsrc/server/bot/default/bot.qc b/qcsrc/server/bot/default/bot.qc index 5514c7c5d..1d8c4f3a4 100644 --- a/qcsrc/server/bot/default/bot.qc +++ b/qcsrc/server/bot/default/bot.qc @@ -63,7 +63,10 @@ void bot_spawn_setup(entity bot) void bot_remove(entity bot) { currentbots = currentbots - 1; - dropclient(bot); + if ( IS_FAKE_CLIENT(bot) ) + delete(bot); + else + dropclient(bot); } void bot_think(entity this) @@ -110,16 +113,19 @@ void bot_think(entity this) // skill 0 = ping 0.7 (slightly drunk) // clear buttons - PHYS_INPUT_BUTTON_ATCK(this) = false; - PHYS_INPUT_BUTTON_JUMP(this) = false; - PHYS_INPUT_BUTTON_ATCK2(this) = false; - PHYS_INPUT_BUTTON_ZOOM(this) = false; - PHYS_INPUT_BUTTON_CROUCH(this) = false; - PHYS_INPUT_BUTTON_HOOK(this) = false; - PHYS_INPUT_BUTTON_INFO(this) = false; - PHYS_INPUT_BUTTON_DRAG(this) = false; - PHYS_INPUT_BUTTON_CHAT(this) = false; - PHYS_INPUT_BUTTON_USE(this) = false; + if ( !IS_FAKE_CLIENT(this) ) + { + PHYS_INPUT_BUTTON_ATCK(this) = false; + PHYS_INPUT_BUTTON_JUMP(this) = false; + PHYS_INPUT_BUTTON_ATCK2(this) = false; + PHYS_INPUT_BUTTON_ZOOM(this) = false; + PHYS_INPUT_BUTTON_CROUCH(this) = false; + PHYS_INPUT_BUTTON_HOOK(this) = false; + PHYS_INPUT_BUTTON_INFO(this) = false; + PHYS_INPUT_BUTTON_DRAG(this) = false; + PHYS_INPUT_BUTTON_CHAT(this) = false; + PHYS_INPUT_BUTTON_USE(this) = false; + } if (time < game_starttime) { @@ -133,7 +139,7 @@ void bot_think(entity this) if (IS_DEAD(this)) { this.movement = '0 0 0'; - if (this.deadflag == DEAD_DEAD) + if (this.deadflag == DEAD_DEAD && !IS_FAKE_CLIENT(this)) { PHYS_INPUT_BUTTON_JUMP(this) = true; // press jump to respawn this.bot_strategytime = 0; @@ -399,7 +405,7 @@ void bot_relinkplayerlist() void bot_clientdisconnect(entity this) { - if (!IS_BOT_CLIENT(this)) + if (IS_REAL_CLIENT(this)) return; bot_clearqueue(this); if(this.cleanname) @@ -422,7 +428,7 @@ void bot_clientdisconnect(entity this) void bot_clientconnect(entity this) { - if (!IS_BOT_CLIENT(this)) return; + if (IS_REAL_CLIENT(this)) return; this.bot_preferredcolors = this.clientcolors; this.bot_nextthink = time - random(); this.lag_func = bot_lagfunc; @@ -500,16 +506,8 @@ void bot_removenewest() best = M_ARGV(0, entity); if ( best ) { - if ( !IS_BOT_CLIENT(best) ) - { - LOG_WARN("Mutator selected a non-bot as a bot to remove\n"); - best = NULL; - } - else - { - bot_remove(best); - return; - } + bot_remove(best); + return; } if(teamplay) @@ -605,7 +603,7 @@ float bot_fixcount() { int activerealplayers = 0; int realplayers = 0; - int bots = 0; + int bots = -1; if (MUTATOR_CALLHOOK(Bot_FixCount, activerealplayers, realplayers, bots)) { activerealplayers = M_ARGV(0, int); @@ -624,7 +622,7 @@ float bot_fixcount() // But don't remove bots immediately on level change, as the real players // usually haven't rejoined yet bots_would_leave = false; - if (bots) + if (bots >= 0) { // Nothing to do, number of bots set by the hook } diff --git a/qcsrc/server/client.qc b/qcsrc/server/client.qc index 9d5be4c80..89ea7f3eb 100644 --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@ -491,12 +491,12 @@ void FixPlayermodel(entity player) /** Called when a client spawns in the server */ void PutClientInServer(entity this) { - if (IS_BOT_CLIENT(this)) { - TRANSMUTE(Player, this); - } else if (IS_REAL_CLIENT(this)) { + if (IS_REAL_CLIENT(this)) { msg_entity = this; WriteByte(MSG_ONE, SVC_SETVIEW); WriteEntity(MSG_ONE, this); + } else { + TRANSMUTE(Player, this); } if (game_stopped) TRANSMUTE(Observer, this); diff --git a/qcsrc/server/mutators/mutator/gamemode_singleplayer.qc b/qcsrc/server/mutators/mutator/gamemode_singleplayer.qc index 371f906af..a91692d44 100644 --- a/qcsrc/server/mutators/mutator/gamemode_singleplayer.qc +++ b/qcsrc/server/mutators/mutator/gamemode_singleplayer.qc @@ -43,17 +43,22 @@ spawnfunc(info_player_singleplayer) spawnfunc_info_player_deathmatch(this); } +.bool sp_hack; /* * Creates a new bot and assigns it to the given spawn point */ void sp_spawn_bot(entity spawn_point) { sp_bot_number++; - entity bot = spawnclient(); + entity bot = spawn(); if (bot) { bot.sp_spawn_spot = spawn_point; +// bot.flags |= FL_CLIENT; bot_spawn_setup(bot); + //bot.origin = spawn_point.origin; + bot.team = SP_TEAM_ENEMY; + bot.sp_hack = true; } else { @@ -109,6 +114,7 @@ void sp_delayed_init(entity this) // Ensures the given bot will be removed void sp_remove_bot(entity bot) { + bot.netname = "killed"; sp_bot_number--; bot.sp_spawn_spot = NULL; bot_clear(bot); @@ -123,11 +129,11 @@ MUTATOR_HOOKFUNCTION(sp, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE) { M_ARGV(1, string) = "sp_team"; entity ent = M_ARGV(2, entity); - if ( IS_BOT_CLIENT(ent) ) + if ( !IS_REAL_CLIENT(ent) ) { ent.team_forced = NUM_TEAM_2; } - else if( IS_REAL_CLIENT(ent) ) + else { ent.team_forced = NUM_TEAM_1; c1 = 1; @@ -152,12 +158,20 @@ MUTATOR_HOOKFUNCTION(sp, PlayerSpawn) { entity player = M_ARGV(0, entity); entity spawn_spot = M_ARGV(1, entity); - if ( IS_BOT_CLIENT(player) ) + if ( !IS_REAL_CLIENT(player) ) { + if ( player.sp_spawn_spot != spawn_spot ) + { + player.netname = "derp"; + return; + } + player.can_drop_weapon = spawn_spot.can_drop_weapon; player.items |= IT_UNLIMITED_WEAPON_AMMO; if ( spawn_spot.health ) player.health = spawn_spot.health; + else + player.health = 100; player.armorvalue = spawn_spot.armorvalue; player.weapons = WepSet_FromWeapon(Weapons_fromstr(spawn_spot.weapon_name)); if ( spawn_spot.netname ) @@ -195,7 +209,7 @@ MUTATOR_HOOKFUNCTION(sp, Spawn_Score) entity player = M_ARGV(0, entity); entity spawn_spot = M_ARGV(1, entity); vector spawn_score = M_ARGV(2, vector); - if ( IS_BOT_CLIENT(player) ) + if ( !IS_REAL_CLIENT(player) ) { if ( spawn_spot.sp_spawn_team != SP_TEAM_ENEMY || (player.sp_spawn_spot && player.sp_spawn_spot != spawn_spot) ) @@ -221,7 +235,7 @@ MUTATOR_HOOKFUNCTION(sp, HideTeamNagger) */ MUTATOR_HOOKFUNCTION(sp, Bot_FixCount) { - M_ARGV(2, int) = sp_bot_number; + M_ARGV(2, int) = 0; return true; } @@ -229,7 +243,7 @@ MUTATOR_HOOKFUNCTION(sp, Bot_FixCount) MUTATOR_HOOKFUNCTION(sp, PlayerDies) { entity target = M_ARGV(2, entity); - if ( IS_BOT_CLIENT(target) ) + if ( !IS_REAL_CLIENT(target) ) { if ( target.sp_spawn_spot && !target.sp_spawn_spot.can_respawn ) { @@ -255,7 +269,7 @@ MUTATOR_HOOKFUNCTION(sp, Bot_SelectRemove) MUTATOR_HOOKFUNCTION(sp, PlayerPreThink) { entity player = M_ARGV(0, entity); - if ( IS_BOT_CLIENT(player) && !player.sp_spawn_spot ) + if ( !IS_REAL_CLIENT(player) && !player.sp_spawn_spot ) { bot_remove(player); } @@ -265,7 +279,7 @@ MUTATOR_HOOKFUNCTION(sp, PlayerPreThink) MUTATOR_HOOKFUNCTION(sp, ItemTouch) { entity toucher = M_ARGV(1, entity); - if ( IS_BOT_CLIENT(toucher) && !autocvar_g_sp_allow_bot_pickup ) + if ( !IS_REAL_CLIENT(toucher) && !autocvar_g_sp_allow_bot_pickup ) return MUT_ITEMTOUCH_RETURN; return MUT_ITEMTOUCH_CONTINUE; } @@ -280,3 +294,364 @@ MUTATOR_HOOKFUNCTION(sp, Item_Spawn) } return false; } + +void PlayerPreThinkHack (entity this) +{ + WarpZone_PlayerPhysics_FixVAngle(this); + + STAT(GAMESTARTTIME, this) = game_starttime; + STAT(ROUNDSTARTTIME, this) = round_starttime; + STAT(ALLOW_OLDVORTEXBEAM, this) = autocvar_g_allow_oldvortexbeam; + STAT(LEADLIMIT, this) = autocvar_leadlimit; + + STAT(WEAPONSINMAP, this) = weaponsInMap; + + if (frametime) { + // physics frames: update anticheat stuff + anticheat_prethink(this); + } + + if (blockSpectators && frametime) { + // WORKAROUND: only use dropclient in server frames (frametime set). + // Never use it in cl_movement frames (frametime zero). + checkSpectatorBlock(this); + } + + zoomstate_set = false; + + // Check for nameless players + /*if (isInvisibleString(this.netname)) { + this.netname = strzone(sprintf("Player#%d", this.playerid)); + // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe? + } + if (this.netname != this.netname_previous) { + if (autocvar_sv_eventlog) { + GameLogEcho(strcat(":name:", ftos(this.playerid), ":", playername(this, false))); + } + if (this.netname_previous) strunzone(this.netname_previous); + this.netname_previous = strzone(this.netname); + }*/ + + // version nagging + /*if (this.version_nagtime && this.cvar_g_xonoticversion && time > this.version_nagtime) { + this.version_nagtime = 0; + if (strstrofs(this.cvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(this.cvar_g_xonoticversion, "autobuild", 0) >= 0) { + // git client + } else if (strstrofs(autocvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(autocvar_g_xonoticversion, "autobuild", 0) >= 0) { + // git server + Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_BETA, autocvar_g_xonoticversion, this.cvar_g_xonoticversion); + } else { + int r = vercmp(this.cvar_g_xonoticversion, autocvar_g_xonoticversion); + if (r < 0) { // old client + Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OUTDATED, autocvar_g_xonoticversion, this.cvar_g_xonoticversion); + } else if (r > 0) { // old server + Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OLD, autocvar_g_xonoticversion, this.cvar_g_xonoticversion); + } + } + }*/ + + // GOD MODE info + if (!(this.flags & FL_GODMODE) && this.max_armorvalue) + { + Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_GODMODE_OFF, this.max_armorvalue); + this.max_armorvalue = 0; + } + + if (STAT(FROZEN, this) == 2) + { + this.revive_progress = bound(0, this.revive_progress + frametime * this.revive_speed, 1); + this.health = max(1, this.revive_progress * start_health); + this.iceblock.alpha = bound(0.2, 1 - this.revive_progress, 1); + + if (this.revive_progress >= 1) + Unfreeze(this); + } + else if (STAT(FROZEN, this) == 3) + { + this.revive_progress = bound(0, this.revive_progress - frametime * this.revive_speed, 1); + this.health = max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * this.revive_progress ); + + if (this.health < 1) + { + if (this.vehicle) + vehicles_exit(this.vehicle, VHEF_RELEASE); + if(this.event_damage) + this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, this.origin, '0 0 0'); + } + else if (this.revive_progress <= 0) + Unfreeze(this); + } + + MUTATOR_CALLHOOK(PlayerPreThink, this); + + /*if(autocvar_g_vehicles_enter && (time > this.last_vehiclecheck) && !game_stopped && !this.vehicle) + if(IS_PLAYER(this) && !STAT(FROZEN, this) && !IS_DEAD(this)) + { + FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_vehicles_enter_radius, IS_VEHICLE(it), + { + if(!IS_DEAD(it) && it.takedamage != DAMAGE_NO) + if((it.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(it.owner, this)) + { + Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_GUNNER); + } + else if(!it.owner) + { + if(!it.team || SAME_TEAM(this, it)) + Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER); + else if(autocvar_g_vehicles_steal) + Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_STEAL); + } + }); + + this.last_vehiclecheck = time + 1; + }*/ + + if(!this.cvar_cl_newusekeysupported) // FIXME remove this - it was a stupid idea to begin with, we can JUST use the button + { + if(PHYS_INPUT_BUTTON_USE(this) && !this.usekeypressed) + PlayerUseKey(this); + this.usekeypressed = PHYS_INPUT_BUTTON_USE(this); + } + + /*if (IS_REAL_CLIENT(this)) + PrintWelcomeMessage(this);*/ + + if (IS_PLAYER(this)) { + CheckRules_Player(this); + + if (game_stopped || intermission_running) { + this.modelflags &= ~MF_ROCKET; + if(intermission_running) + IntermissionThink(this); + return; + } + + if (timeout_status == TIMEOUT_ACTIVE) { + // don't allow the player to turn around while game is paused + // FIXME turn this into CSQC stuff + this.v_angle = this.lastV_angle; + this.angles = this.lastV_angle; + this.fixangle = true; + } + + if (frametime) player_powerups(this); + + if (IS_DEAD(this)) { + if (this.personal && g_race_qualifying) { + if (time > this.respawn_time) { + STAT(RESPAWN_TIME, this) = this.respawn_time = time + 1; // only retry once a second + respawn(this); + this.impulse = CHIMPULSE_SPEEDRUN.impulse; + } + } else { + if (frametime) player_anim(this); + bool button_pressed = (PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this) || PHYS_INPUT_BUTTON_ATCK2(this) || PHYS_INPUT_BUTTON_HOOK(this) || PHYS_INPUT_BUTTON_USE(this)); + + switch(this.deadflag) + { + case DEAD_DYING: + { + if ((this.respawn_flags & RESPAWN_FORCE) && !(this.respawn_time < this.respawn_time_max)) + this.deadflag = DEAD_RESPAWNING; + else if (!button_pressed || (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE))) + this.deadflag = DEAD_DEAD; + break; + } + case DEAD_DEAD: + { + if (button_pressed) + this.deadflag = DEAD_RESPAWNABLE; + else if (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE)) + this.deadflag = DEAD_RESPAWNING; + break; + } + case DEAD_RESPAWNABLE: + { + if (!button_pressed || (this.respawn_flags & RESPAWN_FORCE)) + this.deadflag = DEAD_RESPAWNING; + break; + } + case DEAD_RESPAWNING: + { + if (time > this.respawn_time) + { + this.respawn_time = time + 1; // only retry once a second + this.respawn_time_max = this.respawn_time; + respawn(this); + } + break; + } + } + + ShowRespawnCountdown(this); + + if (this.respawn_flags & RESPAWN_SILENT) + STAT(RESPAWN_TIME, this) = 0; + else if ((this.respawn_flags & RESPAWN_FORCE) && this.respawn_time < this.respawn_time_max) + { + if (time < this.respawn_time) + STAT(RESPAWN_TIME, this) = this.respawn_time; + else if (this.deadflag != DEAD_RESPAWNING) + STAT(RESPAWN_TIME, this) = -this.respawn_time_max; + } + else + STAT(RESPAWN_TIME, this) = this.respawn_time; + } + + // if respawning, invert stat_respawn_time to indicate this, the client translates it + if (this.deadflag == DEAD_RESPAWNING && STAT(RESPAWN_TIME, this) > 0) + STAT(RESPAWN_TIME, this) *= -1; + + return; + } + + this.prevorigin = this.origin; + + bool have_hook = false; + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + if(this.(weaponentity).hook.state) + { + have_hook = true; + break; + } + } + bool do_crouch = PHYS_INPUT_BUTTON_CROUCH(this); + if (have_hook) { + do_crouch = false; + } else if (this.waterlevel >= WATERLEVEL_SWIMMING) { + do_crouch = false; + } else if (this.vehicle) { + do_crouch = false; + } else if (STAT(FROZEN, this)) { + do_crouch = false; + } + + if (do_crouch) { + if (!this.crouch) { + this.crouch = true; + this.view_ofs = STAT(PL_CROUCH_VIEW_OFS, this); + setsize(this, STAT(PL_CROUCH_MIN, this), STAT(PL_CROUCH_MAX, this)); + // setanim(this, this.anim_duck, false, true, true); // this anim is BROKEN anyway + } + } else if (this.crouch) { + tracebox(this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), this.origin, false, this); + if (!trace_startsolid) { + this.crouch = false; + this.view_ofs = STAT(PL_VIEW_OFS, this); + setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this)); + } + } + + FixPlayermodel(this); + + // LordHavoc: allow firing on move frames (sub-ticrate), this gives better timing on slow servers + //if(frametime) + { + this.items &= ~this.items_added; + + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + W_WeaponFrame(this, weaponentity); + + if(slot == 0) + { + this.clip_load = this.(weaponentity).clip_load; + this.clip_size = this.(weaponentity).clip_size; + } + } + + this.items_added = 0; + if (this.items & ITEM_Jetpack.m_itemid && (this.items & ITEM_JetpackRegen.m_itemid || this.ammo_fuel >= 0.01)) + this.items_added |= IT_FUEL; + + this.items |= this.items_added; + } + + player_regen(this); + + // WEAPONTODO: Add a weapon request for this + // rot vortex charge to the charge limit + /*for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + if (WEP_CVAR(vortex, charge_rot_rate) && this.(weaponentity).vortex_charge > WEP_CVAR(vortex, charge_limit) && this.(weaponentity).vortex_charge_rottime < time) + this.(weaponentity).vortex_charge = bound(WEP_CVAR(vortex, charge_limit), this.(weaponentity).vortex_charge - WEP_CVAR(vortex, charge_rot_rate) * frametime / W_TICSPERFRAME, 1); + }*/ + + if (frametime) player_anim(this); + + // secret status + //secrets_setstatus(this); + + // monsters status + //monsters_setstatus(this); + + this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime); + } + else if (game_stopped || intermission_running) { + if(intermission_running) + IntermissionThink(this); + return; + } + /*else if (IS_OBSERVER(this)) { + ObserverThink(this); + } + else if (IS_SPEC(this)) { + SpectatorThink(this); + }*/ + + // WEAPONTODO: Add weapon request for this + /*if (!zoomstate_set) { + bool wep_zoomed = false; + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + Weapon thiswep = this.(weaponentity).m_weapon; + if(thiswep != WEP_Null && thiswep.wr_zoom) + wep_zoomed += thiswep.wr_zoom(thiswep, this); + } + SetZoomState(this, PHYS_INPUT_BUTTON_ZOOM(this) || PHYS_INPUT_BUTTON_ZOOMSCRIPT(this) || wep_zoomed); + }*/ + + /*if (this.teamkill_soundtime && time > this.teamkill_soundtime) + { + this.teamkill_soundtime = 0; + + entity e = this.teamkill_soundsource; + entity oldpusher = e.pusher; + e.pusher = this; + PlayerSound(e, playersound_teamshoot, CH_VOICE, VOL_BASEVOICE, VOICETYPE_LASTATTACKER_ONLY); + e.pusher = oldpusher; + } + + if (this.taunt_soundtime && time > this.taunt_soundtime) { + this.taunt_soundtime = 0; + PlayerSound(this, playersound_taunt, CH_VOICE, VOL_BASEVOICE, VOICETYPE_AUTOTAUNT); + } + + target_voicescript_next(this); + + // WEAPONTODO: Move into weaponsystem somehow + // if a player goes unarmed after holding a loaded weapon, empty his clip size and remove the crosshair ammo ring + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + if(this.(weaponentity).m_weapon == WEP_Null) + this.(weaponentity).clip_load = this.(weaponentity).clip_size = 0; + }*/ +} + + +MUTATOR_HOOKFUNCTION(sp, SV_StartFrame) +{ + entity e = NULL; + while( (e = findfloat(e, sp_hack, true)) ) + { +// PlayerPreThink(e); + PlayerPreThinkHack(e); + + } +}