/*
AMX Mod X script.
│ Author : Arkshine
│ Plugin : Custom Air Accelerate
│ Version : v2.0
This plugin is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at
your option) any later version.
This plugin is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this plugin; if not, write to the Free Software Foundation,
Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
│ DESCRIPTION
With this plugin, a player can now have a custom air accelerate value.
An API is available too for others plugins.
│ REQUIREMENT
* CS 1.6 / CZ.
* AMX Mod X 1.8.x or higher.
* Orpheu 2.4 and higher.
* Cvar Utilities 1.6 and higher.
│ API
* native set_user_airaccelerate( player, const Float:airaccelerate );
Sets a player's personal value for airaccelerate.
Returns the value for airaccelerate before setting to new value.
* native get_user_airaccelerate( player );
Gets a player's personal value for airaccelerate.
Returns the value for airaccelerate.
* native reset_user_airaccelerate( player );
Resets a player's personal value for airaccelerate.
Use 0 as player to reset all players' airaccelerate.
*/
#include <amxmodx>
#tryinclude <orpheu>
#tryinclude <orpheu_stocks>
#tryinclude <cvar_util>
#if !defined _cvar_util_included
#assert "cvar_util.inc library required ! Download it at :
https://forums.alliedmods.net/showthread.php?t=154642#InstallationFiles"
#endif
#if !defined _orpheu_included || !defined _orpheu_advanced_included
#assert "orpheu.inc/orpheu_advanced.inc libraries required! Download them at
https://forums.alliedmods.net/showthread.php?t=116393"
#endif
/*
│ PLUGIN
*/
new const PluginName [] = "Custom Player Air Accelerate";
new const PluginVersion [] = "2.0";
new const PluginAuthor [] = "Arkshine";
/*
│ GENERAL
*/
const MaxSlots = 32;
#define FBitSet(%1,%2) ( %1 & ( 1 << ( %2 & 31 ) ) )
#define SetBits(%1,%2) ( %1 |= ( 1 << ( %2 & 31 ) ) )
#define ClearBits(%1,%2) ( %1 &= ~( 1 << ( %2 & 31 ) ) )
#define IsPlayer(%1) ( 1 <= %1 <= MaxClients )
new MaxClients;
/*
│ CACHE HANDLING
*/
enum Movevars
{
Float:Gravity,
Float:StopSpeed,
Float:MaxSpeed,
Float:SpectatorMaxSpeed,
Float:Accelerate,
Float:AirAccelerate,
Float:WaterAccelerate,
Float:Friction,
Float:EdgeFriction,
Float:WaterFriction,
Float:EntGravity,
Float:Bounce,
Float:StepSize,
Float:MaxVelocity,
Float:ZMax,
Float:WaveHeigth,
bool:Footsteps,
Float:RollAngle,
Float:RollSpeed,
Float:SkyColorRed,
Float:SkyColorGreen,
Float:SkyColorBlue,
Float:SkyVecX,
Float:SkyVecY,
Float:SkyVecZ,
SkyName[ 16 ],
};
new MoveVarsData[ Movevars ];
/*
│ AIR ACCELERATE HANDLING
*/
new const StructMemberPlayerIndex [] = "player_index";
new const StructMemberMovevars [] = "movevars";
new const StructMemberAirAccelerate[] = "airaccelerate";
new OrpheuStruct:pmove;
new OrpheuFunction:HandleFuncMove;
new OrpheuFunction:HandleFuncAirAccelerate;
new OrpheuFunction:HandleFuncAirMove;
new OrpheuFunction:HandleFuncQueryMovevarsChanged;
new OrpheuFunction:HandleFuncSetMoveVars;
new OrpheuFunction:HandleFuncWriteMovevarsToClient;
new bool:ChangeDetected;
new HasCustomAirAccel;
new Float:PlayerCustomAAValue[ MaxSlots + 1 ];
public plugin_init()
{
register_plugin( PluginName, PluginVersion, PluginAuthor );
handleCommand();
handleCache();
handleForward();
}
public plugin_natives()
{
register_library( "airaccelerate" );
register_native( "set_user_airaccelerate" , "Native_SetUserAiraccelerate" );
register_native( "get_user_airaccelerate" , "Native_GetUserAiraccelerate" );
register_native( "reset_user_airaccelerate" , "Native_ResetUserAiraccelerate" );
}
public plugin_cfg()
{
MaxClients = get_maxplayers();
}
public client_disconnect( client )
{
resetPlayerAA( client, .disconnected = true );
}
/*
│ ┌────────────────────────┐
│ │ INIT FUNCTIONS │
│ └────────────────────────┘
│ → handleCommand()
│ → handleCache()
│ → handleForward()
*/
handleCommand()
{
register_clcmd( "say", "ClientCommand_Say" );
register_clcmd( "say_team", "ClientCommand_Say" );
}
handleCache()
{
// Easily cache the value with Cvar Utilities. ;P
CvarCache( get_cvar_pointer( "sv_gravity" ), CvarType_Float , MoveVarsData[ Gravity ] );
CvarCache( get_cvar_pointer( "sv_stopspeed" ), CvarType_Float , MoveVarsData[ StopSpeed ] );
CvarCache( get_cvar_pointer( "sv_maxspeed" ), CvarType_Float , MoveVarsData[ MaxSpeed ] );
CvarCache( get_cvar_pointer( "sv_spectatormaxspeed" ), CvarType_Float , MoveVarsData[ SpectatorMaxSpeed ] );
CvarCache( get_cvar_pointer( "sv_accelerate" ), CvarType_Float , MoveVarsData[ Accelerate ] );
CvarCache( get_cvar_pointer( "sv_airaccelerate" ), CvarType_Float , MoveVarsData[ AirAccelerate ] );
CvarCache( get_cvar_pointer( "sv_wateraccelerate" ), CvarType_Float , MoveVarsData[ WaterAccelerate ] );
CvarCache( get_cvar_pointer( "sv_friction" ), CvarType_Float , MoveVarsData[ Friction ] );
CvarCache( get_cvar_pointer( "edgefriction" ), CvarType_Float , MoveVarsData[ EdgeFriction ] );
CvarCache( get_cvar_pointer( "sv_waterfriction" ), CvarType_Float , MoveVarsData[ WaterFriction ] );
CvarCache( get_cvar_pointer( "sv_bounce" ), CvarType_Float , MoveVarsData[ Bounce ] );
CvarCache( get_cvar_pointer( "sv_stepsize" ), CvarType_Float , MoveVarsData[ StepSize ] );
CvarCache( get_cvar_pointer( "sv_maxvelocity" ), CvarType_Float , MoveVarsData[ MaxVelocity ] );
CvarCache( get_cvar_pointer( "sv_zmax" ), CvarType_Float , MoveVarsData[ ZMax ] );
CvarCache( get_cvar_pointer( "sv_wateramp" ), CvarType_Float , MoveVarsData[ WaveHeigth ] );
CvarCache( get_cvar_pointer( "mp_footsteps" ), CvarType_Float , MoveVarsData[ Footsteps ] );
CvarCache( get_cvar_pointer( "sv_skycolor_r" ), CvarType_Float , MoveVarsData[ SkyColorBlue ] );
CvarCache( get_cvar_pointer( "sv_skycolor_g" ), CvarType_Float , MoveVarsData[ SkyColorGreen ] );
CvarCache( get_cvar_pointer( "sv_skycolor_b" ), CvarType_Float , MoveVarsData[ SkyColorRed ] );
CvarCache( get_cvar_pointer( "sv_skyvec_x" ), CvarType_Float , MoveVarsData[ SkyVecX ] );
CvarCache( get_cvar_pointer( "sv_skyvec_y" ), CvarType_Float , MoveVarsData[ SkyVecY ] );
CvarCache( get_cvar_pointer( "sv_skyvec_z" ), CvarType_Float , MoveVarsData[ SkyVecZ ] );
CvarCache( get_cvar_pointer( "sv_skyname" ), CvarType_String, MoveVarsData[ SkyName ], charsmax( MoveVarsData[ SkyName ] ) );
// No existing server cvars, hardcoded value.
MoveVarsData[ EntGravity ] = _:1.0;
MoveVarsData[ RollAngle ] = _:0.0;
MoveVarsData[ RollSpeed ] = _:0.0;
}
handleForward()
{
// Mod -
{
HandleFuncMove = OrpheuGetDLLFunction( "pfnPM_Move", "PM_Move" );
if( is_linux_server() && getEngineBuild() / 100 < 59 )
HandleFuncAirMove = OrpheuGetFunction( "PM_AirMove" );
else
HandleFuncAirAccelerate = OrpheuGetFunction( "PM_AirAccelerate" );
}
// Engine - Main function.
{
HandleFuncQueryMovevarsChanged = OrpheuGetFunction( "SV_QueryMovevarsChanged" );
if( is_linux_server() )
{
HandleFuncSetMoveVars = OrpheuGetFunction( "SV_SetMoveVars" );
HandleFuncWriteMovevarsToClient = OrpheuGetFunction( "SV_WriteMovevarsToClient" );
}
else
{
// Trick to avoid to make signatures.
new const startAddress = OrpheuGetFunctionAddress( HandleFuncQueryMovevarsChanged );
new const numCallsToIgnore = 1;
HandleFuncSetMoveVars = OrpheuCreateFunction( OrpheuGetNextCallAtAddress( startAddress, .number = numCallsToIgnore + 1 ), "SV_SetMoveVars" );
HandleFuncWriteMovevarsToClient = OrpheuCreateFunction( OrpheuGetNextCallAtAddress( startAddress, .number = numCallsToIgnore + 2 ), "SV_WriteMovevarsToClient" );
}
}
}
/*
│ ┌────────────────────┐
│ │ FORWARD HANDLING │
│ └────────────────────┘
│ → updateForward()
*/
/**
* @brief Updates forwards state depending if there are players having custom
* air accelerate value or not.
*/
updateForward()
{
// Mod.
static OrpheuHook:handleHookMove;
static OrpheuHook:handleHookAirAccelerate;
static OrpheuHook:handleHookAirMovePre;
static OrpheuHook:handleHookAirMovePost;
// Engine.
static OrpheuHook:handleHookQueryMovevarsChanged;
static OrpheuHook:handleHookSetMoveVars;
static OrpheuHook:handleHookWriteMovevarsToClient;
new foundUsers;
static wasFoundUsers;
// Should the forwards be toggled ?
if( ( foundUsers = !!HasCustomAirAccel ) ^ wasFoundUsers )
{
if( foundUsers ) // There are at least one player and the forwards should be registered.
{
handleHookMove = OrpheuRegisterHook( HandleFuncMove, "PM_Move" );
if( is_linux_server() && getEngineBuild() / 100 < 59 )
{
// Damn. The content of PM_AirAccelerate is integrated in PM_AirMove.
handleHookAirMovePre = OrpheuRegisterHook( HandleFuncAirMove, "PM_AirMove_Pre" , OrpheuHookPre );
handleHookAirMovePost = OrpheuRegisterHook( HandleFuncAirMove, "PM_AirMove_Post", OrpheuHookPost );
}
else
{
handleHookAirAccelerate = OrpheuRegisterHook( HandleFuncAirAccelerate, "PM_AirAccelerate" );
}
handleHookQueryMovevarsChanged = OrpheuRegisterHook( HandleFuncQueryMovevarsChanged , "SV_QueryMovevarsChanged_Post", OrpheuHookPost );
handleHookSetMoveVars = OrpheuRegisterHook( HandleFuncSetMoveVars , "SV_SetMoveVars" );
handleHookWriteMovevarsToClient = OrpheuRegisterHook( HandleFuncWriteMovevarsToClient, "SV_WriteMovevarsToClient" );
}
else // No more players are using the command, the forwards should be stopped now.
{
OrpheuUnregisterHook( handleHookMove );
if( is_linux_server() && getEngineBuild() / 100 < 59 )
{
OrpheuUnregisterHook( handleHookAirMovePre );
OrpheuUnregisterHook( handleHookAirMovePost );
}
else
{
OrpheuUnregisterHook( handleHookAirAccelerate );
}
OrpheuUnregisterHook( handleHookQueryMovevarsChanged );
OrpheuUnregisterHook( handleHookSetMoveVars );
OrpheuUnregisterHook( handleHookWriteMovevarsToClient );
}
}
wasFoundUsers = foundUsers;
}
/*
│ ┌────────────────────┐
│ │ NATIVES HANDLING │
│ └────────────────────┘
│ → Native_SetUserAiraccelerate()
│ → Native_GetUserAiraccelerate()
│ → Native_ResetUserAiraccelerate()
*/
public Float:Native_SetUserAiraccelerate( const plugin, const params )
{
if( params != 2 )
{
return 0.0;
}
new player = get_param( 1 );
if( !IsPlayer( player ) )
{
return 0.0;
}
new Float:newAirAccelerate = get_param_f( 2 );
if( player )
{
new Float:oldAirAccelerate = getPlayerAA( player );
setPlayerAA( player, newAirAccelerate );
return oldAirAccelerate;
}
for( player = 1; player <= MaxClients; player++ )
{
setPlayerAA( player, newAirAccelerate );
}
return 1.0;
}
public Float:Native_GetUserAiraccelerate( const plugin, const params )
{
if( params != 1 )
{
return 0.0;
}
new player = get_param( 1 );
if( !IsPlayer( player ) )
{
return 0.0;
}
return getPlayerAA( player );
}
public Native_ResetUserAiraccelerate( const plugin, const params )
{
if( params != 1 )
{
return 0;
}
new player = get_param( 1 );
if( !IsPlayer( player ) )
{
return 0;
}
if( player )
{
resetPlayerAA( player );
}
else
{
for( player = 1; player <= MaxClients; player++ )
{
resetPlayerAA( player );
}
}
return 1;
}
/*
│ ┌────────────────────┐
│ │ COMMAND HANDLING │
│ └────────────────────┘
│ → ClientCommand_Say()
│ ├ getPlayerAA()
│ ├ setPlayerAA()
│ └ resetPlayerAA()
*/
/**
* @brief Called when a client type something in the chat.
* Handles plugin command /aa and it's params get, reset and <integer>.
*
* @param client The player's index who has typed something in the chat.
*/
public ClientCommand_Say( const client )
{
static commandLine[ 32 ];
if( !read_args( commandLine, charsmax( commandLine ) ) )
{
return PLUGIN_CONTINUE;
}
remove_quotes( commandLine );
new start; skip_spaces( commandLine, start );
if( commandLine[ start ] == '/' && commandLine[ start + 1 ] & ~( 1 << 5 ) == 'A' && commandLine[ start + 2 ] & ~( 1 << 5 ) == 'A' )
{
start += 3; skip_spaces( commandLine, start );
switch( commandLine[ start ] & ~( 1 << 5 ) )
{
case 'G' :
{
client_print( client, print_chat, "* Your current Air Accelerate is : %.2f.", getPlayerAA( client ) );
}
case 'R' :
{
resetPlayerAA( client );
client_print( client, print_chat, "* Your custom Air Accelerate has been reseted to default (%.0f).", MoveVarsData[ AirAccelerate ] );
}
default :
{
if( !isalpha( commandLine[ start ] ) )
{
new Float:aa = Float:str_to_float( commandLine[ start ] );
client_print( client, print_chat, "* New custom Air Accelerate changed from %.2f to %.2f.", getPlayerAA( client ), aa );
setPlayerAA( client, aa );
}
else
{
client_print( client, print_chat, "* Invalid /aa argument : ^"%s^"", commandLine[ start ] );
client_print( client, print_chat, "* Valid arguments : get, reset, <float>" );
}
}
}
return PLUGIN_HANDLED;
}
return PLUGIN_CONTINUE;
}
/**
* @brief Gets the current player's air accelerate value.
*
* @param player The player's index.
*
* @return Player's custom value, otherwise the default server value.
*/
Float:getPlayerAA( const player )
{
return FBitSet( HasCustomAirAccel, player ) ? PlayerCustomAAValue[ player ] : MoveVarsData[ AirAccelerate ];
}
/**
* @brief Resets a player's air accelerate to the default server value.
*
* @param client The player's index.
* @param disconnect Whether the player is already disconnected.
*/
resetPlayerAA( const client, const bool:disconnected = false )
{
ClearBits( HasCustomAirAccel, client );
updateForward();
if( !disconnected )
{
WriteMovevarsToClient( client );
}
}
/**
* @brief Sets a player's air accelerate to the provided value.
*
* @param client The player's index.
* @param aa The new air accelerate value.
*/
setPlayerAA( const player, const Float:aa )
{
PlayerCustomAAValue[ player ] = aa;
SetBits( HasCustomAirAccel, player );
updateForward();
WriteMovevarsToClient( player );
}
/*
│ ┌───────────────────────────┐
│ │ AIR ACCELERATE HANDLING │
│ └───────────────────────────┘
│ → PM_Move()
│ → PM_AirMove_Pre()
│ → PM_AirMove_Post()
│ → PM_AirAccelerate()
│ → SV_SetMoveVars()
│ → SV_WriteMovevarsToClient()
│ → SV_QueryMovevarsChanged_Post()
│ └ WriteMovevarsToClient()
│ ├ checkConnectingClient()
│ └ sendNewMoveVars()
*/
/**
* @brief Called when a player moves.
* Used to retrieve pmove handle to be used in others functions.
*
* @param ppmove
* @param server
*/
public PM_Move( OrpheuStruct:ppmove, const server )
{
pmove = ppmove;
}
/**
* @brief Called when player is moving in the air.
* Used to modify on-the-fly pmove->movevars->airaccelerate to our custom value.
*
* @note Linux specific.
*/
public PM_AirMove_Pre()
{
static player; player = OrpheuGetStructMember( pmove, StructMemberPlayerIndex ) + 1;
if( FBitSet( HasCustomAirAccel, player ) && is_user_alive( player ) )
{
OrpheuSetStructMember( OrpheuStruct:OrpheuGetStructMember( pmove, StructMemberMovevars ), StructMemberAirAccelerate, PlayerCustomAAValue[ player ] );
}
}
/**
* @brief Called when player is moving in the air.
* Used to back the current sv_airaccelerate value to avoid to trigger change.
*
* @note Linux specific.
*/
public PM_AirMove_Post()
{
static player; player = OrpheuGetStructMember( pmove, StructMemberPlayerIndex ) + 1;
if( FBitSet( HasCustomAirAccel, player ) && is_user_alive( player ) )
{
OrpheuSetStructMember( OrpheuStruct:OrpheuGetStructMember( pmove, StructMemberMovevars ), StructMemberAirAccelerate, MoveVarsData[ AirAccelerate ] );
}
}
/**
* @brief Called to calculate the final velocity when a player is moving in the air.
*
* @note pmove->movevars->airaccelerate is passed as 'accel' param.
* More simple to modify directly the function where the movevars is used.
* Also, it would be called only when you are in the air.
*
* @param wishdir The wanted direction. Unused.
* @param wishspeed The wanted speed. Unsued.
* @param accel The air accelerate server value.
*/
public PM_AirAccelerate( const Float:wishdir[3], const Float:wishspeed, const Float:accel )
{
static player; player = OrpheuGetStructMember( pmove, StructMemberPlayerIndex ) + 1;
if( FBitSet( HasCustomAirAccel, player ) && is_user_alive( player ) )
{
OrpheuSetParam( 3, PlayerCustomAAValue[ player ] );
}
}
/**
* @brief Order of calls : SV_QueryMovevarsChanged -> SV_SetMoveVars -> SV_WriteMovevarsToClient
*/
/**
* @brief Copies server vars value to move vars.
*
* @note Called from SV_QueryMovevarsChanged.
* This function overwrites the server movevars values with the value of the cvars.
* We use just this function to detect a change since the main function does not return a value.
*/
public SV_SetMoveVars()
{
ChangeDetected = true;
}
/**
* @brief Sends the actual internal SVC_NEWMOVEVARS message on the client.
*
* @note This function sends the message using the movevars values.
* When a change is detected, this function is called for all players to update them.
* Since the param passed is not the player's index and to have a full control,
* We simply blocks the call all the time and sending manually a message when needed after.
*/
public OrpheuHookReturn:SV_WriteMovevarsToClient()
{
return OrpheuSupercede;
}
/**
* @brief Checks if there is a change between server abd move vars.
*
* @note This function is called per frame to detect if there is at least one change between
* the movevars values and the corresponding server cvars values.
* When a change is detected, it calls SV_SetMoveVars to update all the server movevars,
* then call SV_WriteMovevarsToClieznt to update each clients.
* Here, we hook the function as post because since we block the update on the client, we
* need to update manually the client. It allows us to put conditions and sending per player
* and will handle the case where you use sv_ cvar in the console.
*/
public SV_QueryMovevarsChanged_Post()
{
if( ChangeDetected )
{
ChangeDetected = false;
WriteMovevarsToClient( .client = 0 );
}
}
/**
* @brief Updates the client(s) by sending SVC_NEWMOVEVARS.
*
* @note Such message is originally sent one time at player's connection,
* that's why we check connecting user because the player is not fully in-game yet.
*
* @param client The player's index to send the message.
* Using 0 as index to send for all players.
*/
public WriteMovevarsToClient( const client )
{
new playerToUpdate = client ? client : checkConnectingClient();
if( playerToUpdate )
{
sendNewMoveVars( playerToUpdate );
}
else
{
new playersList[ MaxSlots ];
new playersCount;
get_players( playersList, playersCount, "ach" );
for( new i = 0; i < playersCount; i++ )
{
sendNewMoveVars( i );
}
}
}
/**
* @brief Sends the actual SVC_NEWMOVEVARS message to a specific client.
*
* @param client The player's index to send the message.
*/
sendNewMoveVars( const client )
{
message_begin( MSG_ONE, SVC_NEWMOVEVARS, .player = client );
{
write_long( _:MoveVarsData[ Gravity ] );
write_long( _:MoveVarsData[ StopSpeed ] );
write_long( _:MoveVarsData[ MaxSpeed ] );
write_long( _:MoveVarsData[ SpectatorMaxSpeed ] );
write_long( _:MoveVarsData[ Accelerate ] );
write_long( _
FBitSet( HasCustomAirAccel, client ) ? PlayerCustomAAValue[ client ] : MoveVarsData[ AirAccelerate ] ) );
write_long( _:MoveVarsData[ WaterAccelerate ] );
write_long( _:MoveVarsData[ Friction ] );
write_long( _:MoveVarsData[ EdgeFriction ] );
write_long( _:MoveVarsData[ WaterFriction ] );
write_long( _:MoveVarsData[ EntGravity ] );
write_long( _:MoveVarsData[ Bounce ] );
write_long( _:MoveVarsData[ StepSize ] );
write_long( _:MoveVarsData[ MaxVelocity ] );
write_long( _:MoveVarsData[ ZMax ] );
write_long( _:MoveVarsData[ WaveHeigth ] );
write_byte( !!MoveVarsData[ Footsteps ] );
write_long( _:MoveVarsData[ RollAngle ] );
write_long( _:MoveVarsData[ RollSpeed ] );
write_long( _:MoveVarsData[ SkyColorBlue ] );
write_long( _:MoveVarsData[ SkyColorGreen ] );
write_long( _:MoveVarsData[ SkyColorRed ] );
write_long( _:MoveVarsData[ SkyVecX ] );
write_long( _:MoveVarsData[ SkyVecY ] );
write_long( _:MoveVarsData[ SkyVecZ ] );
write_string( MoveVarsData[ SkyName ] );
}
message_end();
}
/*
│ ┌──────────────────┐
│ │ MISC FUNCTIONS │
│ └──────────────────┘
│ → checkConnectingClient()
│ → getEngineBuild()
│ → skip_spaces()
*/
/**
* @brief Checks the connectings users.
* @return The first player's index who is connecting to the server.
*/
checkConnectingClient()
{
for( new i = 1; i <= MaxClients; i++ )
{
if( is_user_connecting( i ) )
{
return i;
}
}
return 0;
}
/**
* @brief Gets the engine build version from sv_version server cvar.
* @return The engine build version.
*/
getEngineBuild()
{
static build;
if( ! build )
{
new version[ 32 ];
get_pcvar_string( get_cvar_pointer( "sv_version" ), version, charsmax( version ) );
new length = strlen( version );
while( version[ --length ] != ',' ) {}
build = str_to_num( version[ length + 1 ] );
}
return build;
}
/**
* @brief Skips spaces for a given string.
*
* @param string The string to check.
* @param offset The number of found spaces added by reference.
*/
skip_spaces( const string[], &offset )
{
while( string[ offset ] == ' ' ) { offset++; }
}