diff options
Diffstat (limited to 'Northstar.CustomServers')
7 files changed, 161 insertions, 131 deletions
diff --git a/Northstar.CustomServers/mod/cfg/autoexec_ns_server.cfg b/Northstar.CustomServers/mod/cfg/autoexec_ns_server.cfg index 3103f3e7..7c0b9230 100644 --- a/Northstar.CustomServers/mod/cfg/autoexec_ns_server.cfg +++ b/Northstar.CustomServers/mod/cfg/autoexec_ns_server.cfg @@ -8,5 +8,5 @@ ns_auth_allow_insecure 0 // keep this to 0 unless you want to allow people to jo ns_erase_auth_info 1 // keep this to 1 unless you're testing and crashing alot, so you don't have to go through the northstar lobby to reauth ns_player_auth_port 8081 // this can be whatever, make sure it's portforwarded over tcp -ns_masterserver_hostname "192.248.160.3" // masterserver hostname +ns_masterserver_hostname "https://northstar.tf" // masterserver hostname everything_unlocked 1 // unlock everything
\ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut index 58d38ce7..2017b64b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut @@ -19,7 +19,7 @@ void function SvLoadoutsMP_Init() RegisterSignal( "EndUpdateCachedLoadouts" ) RegisterSignal( "GracePeriodDone" ) // temp to get weapons\_weapon_utility.nut:2271 to behave - AddCallback_OnClientConnected( UpdateCallsignOnConnect ) + AddCallback_OnClientConnected( LoadoutsMPInitPlayer ) AddClientCommandCallback( "RequestPilotLoadout", ClientCommandCallback_RequestPilotLoadout ) AddClientCommandCallback( "RequestTitanLoadout", ClientCommandCallback_RequestTitanLoadout ) @@ -38,7 +38,7 @@ void function SvLoadoutsMP_Init() AddClientCommandCallback( "InGameMPMenuClosed", ClientCommandCallback_InGameMPMenuClosed ) AddClientCommandCallback( "LoadoutMenuClosed", ClientCommandCallback_LoadoutMenuClosed ) } - + AddCallback_OnPlayerKilled( DestroyDroppedWeapon ) } @@ -70,8 +70,10 @@ TitanLoadoutDef function GetTitanLoadoutForPlayer( entity player ) return def } -void function UpdateCallsignOnConnect( entity player ) +void function LoadoutsMPInitPlayer( entity player ) { + player.s.loadoutDirty <- false + // these netints are required for callsigns and such to display correctly on other clients player.SetPlayerNetInt( "activeCallingCardIndex", player.GetPersistentVarAsInt( "activeCallingCardIndex" ) ) player.SetPlayerNetInt( "activeCallsignIconIndex", player.GetPersistentVarAsInt( "activeCallsignIconIndex" ) ) @@ -129,6 +131,7 @@ bool function ClientCommandCallback_SetPersistentLoadoutValue( entity player, ar // VERY temp and insecure SetPersistentLoadoutValue( player, args[0], args[1].tointeger(), args[2], val ) + print( args[ 0 ] ) if ( args[0] == "pilot" ) SetPlayerLoadoutDirty( player ) @@ -227,7 +230,7 @@ bool function ClientCommandCallback_LoadoutMenuClosed( entity player, array<stri bool function ClientCommandCallback_InGameMPMenuClosed( entity player, array<string> args ) { - //TryGivePilotLoadoutForGracePeriod( player ) + TryGivePilotLoadoutForGracePeriod( player ) return true } @@ -238,19 +241,31 @@ bool function IsRefValidAndOfType( string ref, int itemType ) void function SetPlayerLoadoutDirty( entity player ) { - if ( file.loadoutGracePeriodEnabled || player.p.usingLoadoutCrate ) - file.dirtyLoadouts.append( player ) + if ( !IsLobby() ) + player.s.loadoutDirty = true } void function TryGivePilotLoadoutForGracePeriod( entity player ) { - if ( !IsLobby() && file.dirtyLoadouts.contains( player ) ) + if ( !IsLobby() && IsAlive( player ) && player.s.loadoutDirty ) { - file.dirtyLoadouts.remove( file.dirtyLoadouts.find( player ) ) + player.s.loadoutDirty = false - if ( Time() - player.s.respawnTime <= CLASS_CHANGE_GRACE_PERIOD || player.p.usingLoadoutCrate ) + // for intros + float respawnTimeReal + if ( GetGameState() == eGameState.Playing && Time() - expect float( GetServerVar( "gameStateChangeTime" ) ) <= CLASS_CHANGE_GRACE_PERIOD ) + respawnTimeReal = expect float( GetServerVar( "gameStateChangeTime" ) ) + else + respawnTimeReal = expect float( player.s.respawnTime ) + + if ( Time() - respawnTimeReal <= CLASS_CHANGE_GRACE_PERIOD || player.p.usingLoadoutCrate || GetGameState() < eGameState.Playing ) { - Loadouts_TryGivePilotLoadout( player ) + // because the game sucks and stuff Loadouts_TryGivePilotLoadout doesn't work in intro so have to do this manually + int loadoutIndex = GetPersistentSpawnLoadoutIndex( player, "pilot" ) + GivePilotLoadout( player, GetPilotLoadoutFromPersistentData( player, loadoutIndex ) ) + SetActivePilotLoadout( player ) + SetActivePilotLoadoutIndex( player, loadoutIndex ) + player.p.usingLoadoutCrate = false } else diff --git a/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut b/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut index 96b84c23..d086d65b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut @@ -136,19 +136,10 @@ void function EvacEpilogueCompleted( entity dropship ) { wait 5.0 - if ( IsValid( dropship ) ) - { - foreach ( entity player in dropship.s.evacSlots ) - { - if ( !IsValid( player ) ) - continue - - ScreenFadeToBlackForever( player, 2.0 ) - } - } + foreach ( entity player in GetPlayerArray() ) + ScreenFadeToBlackForever( player, 2.0 ) wait 2.0 - SetGameState( eGameState.Postmatch ) } @@ -197,6 +188,7 @@ void function Evac( int evacTeam, float initialWait, float arrivalTime, float wa dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) dropship.SetValueForModelKey( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) DispatchSpawn( dropship ) + dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) dropship.s.evacSlots <- [ null, null, null, null, null, null, null, null ] @@ -305,7 +297,7 @@ void function Evac( int evacTeam, float initialWait, float arrivalTime, float wa if ( player.GetTeam() == dropship.GetTeam() ) SetPlayerActiveObjective( player, "EG_DropshipExtractFailedEscape" ) - return + continue } SetPlayerActiveObjective( player, "EG_DropshipExtractSuccessfulEscape" ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_coliseum.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_coliseum.nut index 88a95fe4..fb0270cc 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_coliseum.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_coliseum.nut @@ -31,7 +31,6 @@ void function GamemodeColiseum_Init() Riff_ForceBoostAvailability( eBoostAvailability.Disabled ) Riff_ForceSetEliminationMode( eEliminationMode.Pilots ) SetLoadoutGracePeriodEnabled( false ) // prevent modifying loadouts with grace period - SetWeaponDropsEnabled( false ) ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, ClassicMP_DefaultNoIntro_GetLength() ) AddCallback_GameStateEnter( eGameState.Prematch, ShowColiseumIntroScreen ) @@ -139,6 +138,21 @@ void function SetupColiseumEpilogue() void function RunColiseumOutro() { + // also since this runs on game end, do winstreak stuff + foreach ( entity player in GetPlayerArray() ) + { + if ( GetWinningTeam() == player.GetTeam() ) + { + player.SetPersistentVar( "coliseumTotalWins", player.GetPersistentVarAsInt( "coliseumTotalWins" ) + 1 ) + player.SetPersistentVar( "coliseumWinStreak", player.GetPersistentVarAsInt( "coliseumWinStreak" ) + 1 ) + } + else + { + player.SetPersistentVar( "coliseumTotalWins", maxint( player.GetPersistentVarAsInt( "coliseumTotalWins" ) - 1, 0 ) ) + player.SetPersistentVar( "coliseumWinStreak", 0 ) + } + } + entity outroAnimPoint = GetEnt( "intermission" ) array<entity> winningPlayers = GetPlayerArrayOfTeam( GetWinningTeam() ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut index b6b0aa10..69f67584 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut @@ -20,8 +20,9 @@ const array<string> DROPSHIP_JUMP_ANIMS_POV = [ "Classic_MP_flyin_exit_povA_jump "Classic_MP_flyin_exit_povB_jump", "Classic_MP_flyin_exit_povC_jump", "Classic_MP_flyin_exit_povD_jump" ] + +const int MAX_DROPSHIP_PLAYERS = 4 -const array<int> DROPSHIP_ANIMS_YAW = [ -18, 8, 8, -16 ] global const float DROPSHIP_INTRO_LENGTH = 15.0 // TODO tweak this @@ -30,66 +31,28 @@ struct IntroDropship entity dropship int playersInDropship - entity[4] players + entity[MAX_DROPSHIP_PLAYERS] players } struct { - IntroDropship[2] militiaDropships - IntroDropship[2] imcDropships + // these used to be IntroDropship[2]s but i wanted to be able to use array.getrandom so they have to be actual arrays + array<IntroDropship> militiaDropships + array<IntroDropship> imcDropships float introStartTime - int numPlayersInIntro } file void function ClassicMP_DefaultDropshipIntro_Setup() { - AddCallback_OnClientConnected( DropshipIntro_OnClientConnected ) - AddCallback_OnClientDisconnected( DropshipIntro_OnClientDisconnected ) - + AddCallback_OnClientConnected( DropshipIntro_OnClientConnected ) AddCallback_GameStateEnter( eGameState.Prematch, OnPrematchStart ) } void function DropshipIntro_OnClientConnected( entity player ) { - // find the player's team's dropships - IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships - - // find a dropship with an empty slot - foreach ( IntroDropship dropship in teamDropships ) - if ( dropship.playersInDropship < 4 ) - // we've found a valid dropship - // find an empty player slot - for ( int i = 0; i < dropship.players.len(); i++ ) - if ( dropship.players[ i ] == null ) // empty slot - { - dropship.players[ i ] = player - dropship.playersInDropship++ - - // spawn player into intro if we're already doing intro - if ( GetGameState() == eGameState.Prematch ) - thread SpawnPlayerIntoDropship( player ) - - return - } -} - -void function DropshipIntro_OnClientDisconnected( entity player ) -{ - // find the player's dropship - IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships - - // find the player - foreach ( IntroDropship dropship in teamDropships ) - for ( int i = 0; i < dropship.players.len(); i++ ) - if ( dropship.players[ i ] == player ) - { - // we've found the player, remove them - dropship.players[ i ] = null - dropship.playersInDropship-- - - return - } + if ( GetGameState() == eGameState.Prematch ) + thread SpawnPlayerIntoDropship( player ) } void function OnPrematchStart() @@ -99,16 +62,20 @@ void function OnPrematchStart() print( "starting dropship intro!" ) file.introStartTime = Time() + // make 2 empty dropship structs per team + IntroDropship emptyDropship + file.militiaDropships = [ clone emptyDropship, clone emptyDropship ] + file.imcDropships = [ clone emptyDropship, clone emptyDropship ] + // spawn dropships - array<entity> dropshipSpawns = GetEntArrayByClass_Expensive( "info_spawnpoint_dropship_start" ) - foreach ( entity dropshipSpawn in dropshipSpawns ) + foreach ( entity dropshipSpawn in GetEntArrayByClass_Expensive( "info_spawnpoint_dropship_start" ) ) { if ( GameModeRemove( dropshipSpawn ) || ( GetSpawnpointGamemodeOverride() != GAMETYPE && dropshipSpawn.HasKey( "gamemode_" + GetSpawnpointGamemodeOverride() ) && dropshipSpawn.kv[ "gamemode_" + GetSpawnpointGamemodeOverride() ] == "0" ) ) continue // todo: possibly make this only spawn dropships if we've got enough players to need them - int createTeam = GetServerVar( "switchedSides" ) != 1 ? dropshipSpawn.GetTeam() : GetOtherTeam( dropshipSpawn.GetTeam() ) - IntroDropship[2] teamDropships = createTeam == TEAM_MILITIA ? file.militiaDropships : file.imcDropships + int createTeam = HasSwitchedSides() ? dropshipSpawn.GetTeam() : GetOtherTeam( dropshipSpawn.GetTeam() ) + array<IntroDropship> teamDropships = createTeam == TEAM_MILITIA ? file.militiaDropships : file.imcDropships int dropshipIndex = !IsValid( teamDropships[ 0 ].dropship ) ? 0 : 1 // create entity @@ -116,6 +83,8 @@ void function OnPrematchStart() teamDropships[ dropshipIndex ].dropship = dropship AddAnimEvent( dropship, "dropship_warpout", WarpoutEffect ) + dropship.SetValueForModelKey( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) + dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) DispatchSpawn( dropship ) @@ -128,6 +97,14 @@ void function OnPrematchStart() foreach ( entity player in GetPlayerArray() ) thread SpawnPlayerIntoDropship( player ) + + thread EndIntroWhenFinished() +} + +void function EndIntroWhenFinished() +{ + wait 15.0 + ClassicMP_OnIntroFinished() } void function SpawnPlayerIntoDropship( entity player ) @@ -135,18 +112,37 @@ void function SpawnPlayerIntoDropship( entity player ) if ( IsAlive( player ) ) player.Die() // kill them so we don't have any issues respawning them later + player.s.dropshipIntroIsJumping <- false + OnThreadEnd( function() : ( player ) + { + if ( IsValid( player ) ) + { + player.ClearParent() + ClearPlayerAnimViewEntity( player ) + + if ( !player.s.dropshipIntroIsJumping ) + { + player.MovementEnable() + player.EnableWeaponViewModel() + RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING ) + } + } + }) + WaitFrame() player.EndSignal( "OnDeath" ) player.EndSignal( "OnDestroy" ) - player.EndSignal( "Disconnected" ) - - file.numPlayersInIntro++ // find the player's dropship and seat - IntroDropship[2] teamDropships = player.GetTeam() == TEAM_MILITIA ? file.militiaDropships : file.imcDropships + array<IntroDropship> teamDropships + if ( player.GetTeam() == TEAM_MILITIA ) + teamDropships = file.militiaDropships + else + teamDropships = file.imcDropships + IntroDropship playerDropship - int playerDropshipIndex + int playerDropshipIndex = -1 foreach ( IntroDropship dropship in teamDropships ) for ( int i = 0; i < dropship.players.len(); i++ ) if ( dropship.players[ i ] == player ) @@ -156,29 +152,13 @@ void function SpawnPlayerIntoDropship( entity player ) break } - - if ( playerDropship.dropship == null ) + + if ( true )//if ( playerDropship.dropship == null ) { - // if we're at this point, we have more players than we do dropships, oh dear - ScreenFadeFromBlack( player, 0.0 ) - RespawnAsPilot( player ) - - file.numPlayersInIntro-- - return + // if we're at this point, we have more players than we do dropships, so just pick a random one + playerDropship = teamDropships.getrandom() + playerDropshipIndex = RandomInt( MAX_DROPSHIP_PLAYERS ) } - - // figure out what anims we're using for idle - string idleAnim = DROPSHIP_IDLE_ANIMS[ playerDropshipIndex ] - string idleAnimPov = DROPSHIP_IDLE_ANIMS_POV[ playerDropshipIndex ] - - FirstPersonSequenceStruct idleSequence - idleSequence.firstPersonAnim = idleAnimPov - idleSequence.thirdPersonAnim = idleAnim - idleSequence.attachment = "ORIGIN" - idleSequence.teleport = true - idleSequence.viewConeFunction = ViewConeRampFree - idleSequence.hideProxy = true - idleSequence.setInitialTime = Time() - file.introStartTime // respawn player and holster their weapons so they aren't out player.RespawnPlayer( null ) @@ -189,36 +169,47 @@ void function SpawnPlayerIntoDropship( entity player ) ScreenFadeFromBlack( player, 0.5, 0.5 ) // faction leaders are done clientside, spawn them here Remote_CallFunction_NonReplay( player, "ServerCallback_SpawnFactionCommanderInDropship", playerDropship.dropship.GetEncodedEHandle(), file.introStartTime ) - thread FirstPersonSequence( idleSequence, player, playerDropship.dropship ) - - // wait until the anim is done - WaittillAnimDone( player ) // unsure if this is the best way to do this - // todo: possibly rework this to actually get the time the idle anim takes and start the starttime of the jump sequence for very late joiners using that - - // honestly go rewrite alot of this too it's messy - // figure out what anims we're using for jump - string jumpAnim = DROPSHIP_JUMP_ANIMS[ playerDropshipIndex ] - string jumpAnimPov = DROPSHIP_JUMP_ANIMS_POV[ playerDropshipIndex ] + // do firstperson sequence + FirstPersonSequenceStruct idleSequence + idleSequence.firstPersonAnim = DROPSHIP_IDLE_ANIMS_POV[ playerDropshipIndex ] + idleSequence.thirdPersonAnim = DROPSHIP_IDLE_ANIMS[ playerDropshipIndex ] + idleSequence.attachment = "ORIGIN" + idleSequence.teleport = true + idleSequence.viewConeFunction = ViewConeRampFree + idleSequence.hideProxy = true + idleSequence.setInitialTime = Time() - file.introStartTime + thread FirstPersonSequence( idleSequence, player, playerDropship.dropship ) + WaittillAnimDone( player ) + // todo: possibly rework this to actually get the time the idle anim takes and start the starttime of the jump sequence for very late joiners using that + + // jump sequence FirstPersonSequenceStruct jumpSequence - jumpSequence.firstPersonAnim = jumpAnimPov - jumpSequence.thirdPersonAnim = jumpAnim + jumpSequence.firstPersonAnim = DROPSHIP_JUMP_ANIMS_POV[ playerDropshipIndex ] + jumpSequence.thirdPersonAnim = DROPSHIP_JUMP_ANIMS[ playerDropshipIndex ] jumpSequence.attachment = "ORIGIN" - //jumpSequence.setInitialTime = Time() - ( file.introStartTime + player.GetSequenceDuration( idleAnim ) ) jumpSequence.setInitialTime = Time() - ( file.introStartTime + 10.9 ) // pretty sure you should do this with GetScriptedAnimEventCycleFrac? // idk unsure how to use that, all i know is getsequenceduration > the length it actually should be - thread FirstPersonSequence( jumpSequence, player, playerDropship.dropship ) - WaittillAnimDone( player ) + FirstPersonSequence( jumpSequence, player, playerDropship.dropship ) + + player.s.dropshipIntroIsJumping <- true + thread PlayerJumpsFromDropship( player ) +} - // unparent player and their camera from the dropship - player.ClearParent() - ClearPlayerAnimViewEntity( player ) - - file.numPlayersInIntro-- - if ( file.numPlayersInIntro == 0 ) - ClassicMP_OnIntroFinished() // set intro as finished +void function PlayerJumpsFromDropship( entity player ) +{ + OnThreadEnd( function() : ( player ) + { + if ( IsValid( player ) ) + { + // show weapon viewmodel and hud and let them move again + player.MovementEnable() + player.EnableWeaponViewModel() + RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING ) + } + }) // wait for intro timer to be fully done wait ( file.introStartTime + DROPSHIP_INTRO_LENGTH ) - Time() @@ -229,10 +220,5 @@ void function SpawnPlayerIntoDropship( entity player ) while ( !player.IsOnGround() && !player.IsWallRunning() && !player.IsWallHanging() ) // todo this needs tweaking WaitFrame() - // show weapon viewmodel and hud and let them move again - player.MovementEnable() - player.EnableWeaponViewModel() - RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING ) - TryGameModeAnnouncement( player ) }
\ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut index bf21b492..b2ce4fca 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut @@ -648,9 +648,9 @@ void function CleanUpEntitiesForRoundEnd() foreach ( entity npc in GetNPCArray() ) if ( IsValid( npc ) ) npc.Destroy() // need this because getnpcarray includes the pettitans we just killed at this point - - foreach ( entity weapon in GetEntArrayByClass_Expensive( "weaponx" ) ) - weapon.Destroy() + + // destroy weapons + ClearDroppedWeapons() foreach ( entity battery in GetEntArrayByClass_Expensive( "item_titan_battery" ) ) battery.Destroy() diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut index dca30fe9..592422ee 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut @@ -2,6 +2,29 @@ global function CodeCallback_MapInit void function CodeCallback_MapInit() { - FlagClear( "Disable_Marvins" ) SetupLiveFireMaps() + + // worker drone model + PrecacheModel( $"models/robots/aerial_unmanned_worker/aerial_unmanned_worker.mdl" ) + + // note: this map has no marvin spawns, have to spawn them using idle nodes + AddSpawnCallback_ScriptName( "worker_drone_spawn", DeckSpawnWorkerDrone ) + AddSpawnCallback_ScriptName( "marvin_idle_node", DeckSpawnMarvinForIdleNode ) +} + +void function DeckSpawnWorkerDrone( entity spawnpoint ) +{ + + entity drone = CreateWorkerDrone( TEAM_UNASSIGNED, spawnpoint.GetOrigin(), spawnpoint.GetAngles() ) + DispatchSpawn( drone ) +} + +void function DeckSpawnMarvinForIdleNode( entity node ) +{ + entity marvin = CreateMarvin( TEAM_UNASSIGNED, node.GetOrigin(), node.GetAngles() ) + DispatchSpawn( marvin ) + + // doing this because no ai rn + if ( GetAINScriptVersion() == -1 ) + thread PlayAnim( marvin, node.kv.leveled_animation ) }
\ No newline at end of file |