From 29e00c1ac30bcce39d96192af78567b3cbd76cec Mon Sep 17 00:00:00 2001 From: Shawqeem <1004837646@qq.com> Date: Sat, 1 Apr 2023 23:54:00 +0800 Subject: [PATCH 1/3] feat: :art: Add spectator mode Add spectator mode --- installer/Installer/Model.cs | 1 - logic/Client/MainWindow.xaml.cs | 145 +++++++++++--------- logic/Client/Properties/launchSettings.json | 2 +- 3 files changed, 83 insertions(+), 65 deletions(-) diff --git a/installer/Installer/Model.cs b/installer/Installer/Model.cs index feab124..fb7bf15 100644 --- a/installer/Installer/Model.cs +++ b/installer/Installer/Model.cs @@ -1061,7 +1061,6 @@ namespace WebConnect { string tarfile; //要上传的文件路径 string content; - string filedest; //文件目的地 client.DefaultRequestHeaders.Authorization = new("Bearer", logintoken); Console.WriteLine("请输入要上传的文件完整路径:"); tarfile = Console.ReadLine(); diff --git a/logic/Client/MainWindow.xaml.cs b/logic/Client/MainWindow.xaml.cs index 0d0a918..8d72cc5 100644 --- a/logic/Client/MainWindow.xaml.cs +++ b/logic/Client/MainWindow.xaml.cs @@ -106,6 +106,17 @@ namespace Client } _ = Parser.Default.ParseArguments(args).WithParsed(o => { options = o; }); + if ((args.Length == 3 || args.Length == 4) && options != null && Convert.ToInt64(options.PlayerID) > 2023) + { + spectatorMode = true; + string[] comInfo = new string[3]; + comInfo[0] = options.Ip; + comInfo[1] = options.Port; + comInfo[2] = options.PlayerID; + ConnectToServer(comInfo); + OnReceive(); + return; + } if (options == null || options.cl == false) { OnReceive(); @@ -160,7 +171,7 @@ namespace Client { if (!isPlaybackMode) { - if (comInfo.Length != 5) + if (!spectatorMode && comInfo.Length != 5 || spectatorMode && comInfo.Length != 3) throw new Exception("注册信息有误!"); playerID = Convert.ToInt64(comInfo[2]); Connect.Background = Brushes.Gray; @@ -171,66 +182,69 @@ namespace Client client = new AvailableService.AvailableServiceClient(channel); PlayerMsg playerMsg = new PlayerMsg(); playerMsg.PlayerId = playerID; - playerType = Convert.ToInt64(comInfo[3]) switch - { - 0 => PlayerType.NullPlayerType, - 1 => PlayerType.StudentPlayer, - 2 => PlayerType.TrickerPlayer, - }; - playerMsg.PlayerType = playerType; - if (Convert.ToInt64(comInfo[3]) == 1) - { - humanOrButcher = true; - } - else if (Convert.ToInt64(comInfo[3]) == 2) + if (!spectatorMode) { - humanOrButcher = false; - } - if (playerType == PlayerType.StudentPlayer) - { - switch (Convert.ToInt64(comInfo[4])) + playerType = Convert.ToInt64(comInfo[3]) switch { - case 1: - playerMsg.StudentType = StudentType.Athlete; - break; - case 2: - playerMsg.StudentType = StudentType.Teacher; - break; - case 3: - playerMsg.StudentType = StudentType.StraightAStudent; - break; - case 4: - playerMsg.StudentType = StudentType.Robot; - break; - case 5: - playerMsg.StudentType = StudentType.TechOtaku; - break; - case 0: - default: - playerMsg.StudentType = StudentType.NullStudentType; - break; + 0 => PlayerType.NullPlayerType, + 1 => PlayerType.StudentPlayer, + 2 => PlayerType.TrickerPlayer, + }; + playerMsg.PlayerType = playerType; + if (Convert.ToInt64(comInfo[3]) == 1) + { + humanOrButcher = true; } - } - else if (playerType == PlayerType.TrickerPlayer) - { - switch (Convert.ToInt64(comInfo[4])) + else if (Convert.ToInt64(comInfo[3]) == 2) { - case 1: - playerMsg.TrickerType = TrickerType.Assassin; - break; - case 2: - playerMsg.TrickerType = TrickerType.Klee; - break; - case 3: - playerMsg.TrickerType = TrickerType.ANoisyPerson; - break; - case 4: - playerMsg.TrickerType = TrickerType._4; - break; - case 0: - default: - playerMsg.TrickerType = TrickerType.NullTrickerType; - break; + humanOrButcher = false; + } + if (playerType == PlayerType.StudentPlayer) + { + switch (Convert.ToInt64(comInfo[4])) + { + case 1: + playerMsg.StudentType = StudentType.Athlete; + break; + case 2: + playerMsg.StudentType = StudentType.Teacher; + break; + case 3: + playerMsg.StudentType = StudentType.StraightAStudent; + break; + case 4: + playerMsg.StudentType = StudentType.Robot; + break; + case 5: + playerMsg.StudentType = StudentType.TechOtaku; + break; + case 0: + default: + playerMsg.StudentType = StudentType.NullStudentType; + break; + } + } + else if (playerType == PlayerType.TrickerPlayer) + { + switch (Convert.ToInt64(comInfo[4])) + { + case 1: + playerMsg.TrickerType = TrickerType.Assassin; + break; + case 2: + playerMsg.TrickerType = TrickerType.Klee; + break; + case 3: + playerMsg.TrickerType = TrickerType.ANoisyPerson; + break; + case 4: + playerMsg.TrickerType = TrickerType._4; + break; + case 0: + default: + playerMsg.TrickerType = TrickerType.NullTrickerType; + break; + } } } responseStream = client.AddPlayer(playerMsg); @@ -554,8 +568,8 @@ namespace Client { if (msg.PlayerState == PlayerState.Quit) return false; - //if (playerID >= 2022 || teamID >= 2022) - // return true; + if (spectatorMode) + return true; if (humanOrButcher && human != null) { if (human.Guid == msg.Guid) // 自己能看见自己 @@ -580,8 +594,8 @@ namespace Client private bool CanSee(MessageOfTricker msg) { - // if (playerID >= 2022 || teamID >= 2022) - // return true; + if (spectatorMode) + return true; if (!humanOrButcher && butcher != null) { if (butcher.Guid == msg.Guid) // 自己能看见自己 @@ -606,6 +620,8 @@ namespace Client private bool CanSee(MessageOfProp msg) { + if (spectatorMode) + return true; if (msg.Place == Protobuf.PlaceType.Land) return true; if (humanOrButcher && human != null) @@ -623,6 +639,8 @@ namespace Client private bool CanSee(MessageOfBullet msg) { + if (spectatorMode) + return true; if (msg.Place == Protobuf.PlaceType.Land) return true; if (humanOrButcher && human != null) @@ -769,7 +787,7 @@ namespace Client HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(data.Y * unitWidth / 1000.0 - unitWidth / 2, data.X * unitHeight / 1000.0 - unitHeight / 2, 0, 0), - Fill = Brushes.Red, + //Fill = Brushes.Red, }; switch (data.Type) { @@ -845,7 +863,7 @@ namespace Client }; if (deg == 100) { - icon.Text = "🌟"; + icon.Text = "A+"; } UpperLayerOfMap.Children.Add(icon); } @@ -1343,6 +1361,7 @@ namespace Client private string[] comInfo = new string[5]; ArgumentOptions? options = null; bool gateOpened = false; + bool spectatorMode = false; double coolTime0 = -1, coolTime1 = -1, coolTime2 = -1; const double radiusTimes = 1.0 * Preparation.Utility.GameData.characterRadius / Preparation.Utility.GameData.numOfPosGridPerCell; } diff --git a/logic/Client/Properties/launchSettings.json b/logic/Client/Properties/launchSettings.json index 0d108bf..ca58804 100644 --- a/logic/Client/Properties/launchSettings.json +++ b/logic/Client/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Client": { "commandName": "Project", - "commandLineArgs": " --cl --port 8888 --characterID 0 --type 1 --occupation 1" + "commandLineArgs": " --cl --port 8888 --characterID 3000" } } } \ No newline at end of file From afc12b5cf7accc46f2ae6ec2b4cbffc835c62fba Mon Sep 17 00:00:00 2001 From: Shawqeem <1004837646@qq.com> Date: Sun, 2 Apr 2023 00:11:44 +0800 Subject: [PATCH 2/3] fix: :bug: Add spectator mode judgement Add spectator mode judgment --- logic/Client/MainWindow.xaml.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/logic/Client/MainWindow.xaml.cs b/logic/Client/MainWindow.xaml.cs index 8d72cc5..7b9f6d1 100644 --- a/logic/Client/MainWindow.xaml.cs +++ b/logic/Client/MainWindow.xaml.cs @@ -45,6 +45,7 @@ namespace Client SetStatusBar(); isClientStocked = true; isPlaybackMode = false; + isSpectatorMode = false; drawPicLock = new(); listOfProp = new List(); listOfHuman = new List(); @@ -108,7 +109,7 @@ namespace Client { options = o; }); if ((args.Length == 3 || args.Length == 4) && options != null && Convert.ToInt64(options.PlayerID) > 2023) { - spectatorMode = true; + isSpectatorMode = true; string[] comInfo = new string[3]; comInfo[0] = options.Ip; comInfo[1] = options.Port; @@ -171,7 +172,7 @@ namespace Client { if (!isPlaybackMode) { - if (!spectatorMode && comInfo.Length != 5 || spectatorMode && comInfo.Length != 3) + if (!isSpectatorMode && comInfo.Length != 5 || isSpectatorMode && comInfo.Length != 3) throw new Exception("注册信息有误!"); playerID = Convert.ToInt64(comInfo[2]); Connect.Background = Brushes.Gray; @@ -182,7 +183,7 @@ namespace Client client = new AvailableService.AvailableServiceClient(channel); PlayerMsg playerMsg = new PlayerMsg(); playerMsg.PlayerId = playerID; - if (!spectatorMode) + if (!isSpectatorMode) { playerType = Convert.ToInt64(comInfo[3]) switch { @@ -568,7 +569,7 @@ namespace Client { if (msg.PlayerState == PlayerState.Quit) return false; - if (spectatorMode) + if (isSpectatorMode) return true; if (humanOrButcher && human != null) { @@ -594,7 +595,7 @@ namespace Client private bool CanSee(MessageOfTricker msg) { - if (spectatorMode) + if (isSpectatorMode) return true; if (!humanOrButcher && butcher != null) { @@ -620,7 +621,7 @@ namespace Client private bool CanSee(MessageOfProp msg) { - if (spectatorMode) + if (isSpectatorMode) return true; if (msg.Place == Protobuf.PlaceType.Land) return true; @@ -639,7 +640,7 @@ namespace Client private bool CanSee(MessageOfBullet msg) { - if (spectatorMode) + if (isSpectatorMode) return true; if (msg.Place == Protobuf.PlaceType.Land) return true; @@ -787,7 +788,7 @@ namespace Client HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(data.Y * unitWidth / 1000.0 - unitWidth / 2, data.X * unitHeight / 1000.0 - unitHeight / 2, 0, 0), - //Fill = Brushes.Red, + Fill = Brushes.Red, }; switch (data.Type) { @@ -954,7 +955,7 @@ namespace Client // 键盘控制,未完善 private void KeyBoardControl(object sender, KeyEventArgs e) { - if (!isPlaybackMode) + if (!isPlaybackMode && !isSpectatorMode) { switch (e.Key) { @@ -1133,7 +1134,7 @@ namespace Client //鼠标双击 private void Attack(object sender, RoutedEventArgs e) { - if (!isPlaybackMode) + if (!isPlaybackMode && !isSpectatorMode) { if (humanOrButcher && human != null) { @@ -1361,7 +1362,7 @@ namespace Client private string[] comInfo = new string[5]; ArgumentOptions? options = null; bool gateOpened = false; - bool spectatorMode = false; + bool isSpectatorMode = false; double coolTime0 = -1, coolTime1 = -1, coolTime2 = -1; const double radiusTimes = 1.0 * Preparation.Utility.GameData.characterRadius / Preparation.Utility.GameData.numOfPosGridPerCell; } From f4c6cce70445d617ff0d44ecb1e3ea9864fd1b6e Mon Sep 17 00:00:00 2001 From: Shawqeem <1004837646@qq.com> Date: Sun, 2 Apr 2023 02:02:54 +0800 Subject: [PATCH 3/3] fix: :sparkles: Help the hidden Gate work correctly Help the hidden gate work correctly --- logic/Client/MainWindow.xaml.cs | 39 ++++++++++++++------ logic/Client/PlaybackClient.cs | 12 +++++- logic/Client/StatusBarOfCircumstance.xaml.cs | 19 +++++----- logic/cmd/gameServer.cmd | 16 ++++++-- logic/cmd/playback.cmd | 2 +- 5 files changed, 60 insertions(+), 28 deletions(-) diff --git a/logic/Client/MainWindow.xaml.cs b/logic/Client/MainWindow.xaml.cs index 7b9f6d1..f687207 100644 --- a/logic/Client/MainWindow.xaml.cs +++ b/logic/Client/MainWindow.xaml.cs @@ -57,6 +57,7 @@ namespace Client listOfClassroom = new List(); listOfDoor = new List(); listOfGate = new List(); + listOfHiddenGate = new List(); WindowStartupLocation = WindowStartupLocation.CenterScreen; ReactToCommandline(); } @@ -153,7 +154,7 @@ namespace Client { var pbClient = new PlaybackClient(fileName, pbSpeed); int[,]? map; - if ((map = pbClient.ReadDataFromFile(listOfProp, listOfHuman, listOfButcher, listOfBullet, listOfBombedBullet, listOfAll, listOfChest, listOfClassroom, listOfDoor, listOfGate, drawPicLock)) != null) + if ((map = pbClient.ReadDataFromFile(listOfProp, listOfHuman, listOfButcher, listOfBullet, listOfBombedBullet, listOfAll, listOfChest, listOfClassroom, listOfDoor, listOfHiddenGate, listOfGate, drawPicLock)) != null) { isClientStocked = false; isPlaybackMode = true; @@ -325,14 +326,6 @@ namespace Client mapPatches[i, j].Stroke = Brushes.LightSkyBlue; break;//gate case 10: - foreach (var obj in listOfAll) - { - if (obj.SubjectFinished >= Preparation.Utility.GameData.numOfGeneratorRequiredForEmergencyExit) - { - mapPatches[i, j].Fill = Brushes.LightSalmon; - mapPatches[i, j].Stroke = Brushes.LightSalmon; - } - } break;//emergency case 11: mapPatches[i, j].Fill = Brushes.Gray; @@ -404,6 +397,7 @@ namespace Client listOfChest.Clear(); listOfClassroom.Clear(); listOfDoor.Clear(); + listOfHiddenGate.Clear(); listOfGate.Clear(); MessageToClient content = responseStream.ResponseStream.Current; switch (content.GameState) @@ -508,11 +502,13 @@ namespace Client case MessageOfObj.MessageOfObjOneofCase.GateMessage: listOfGate.Add(obj.GateMessage); break; + case MessageOfObj.MessageOfObjOneofCase.HiddenGateMessage: + listOfHiddenGate.Add(obj.HiddenGateMessage); + break; } } listOfAll.Add(content.AllMessage); break; - case GameState.GameEnd: foreach (var obj in content.ObjMessage) { @@ -545,6 +541,9 @@ namespace Client case MessageOfObj.MessageOfObjOneofCase.GateMessage: listOfGate.Add(obj.GateMessage); break; + case MessageOfObj.MessageOfObjOneofCase.HiddenGateMessage: + listOfHiddenGate.Add(obj.HiddenGateMessage); + break; } } listOfAll.Add(content.AllMessage); @@ -567,7 +566,7 @@ namespace Client //待修改 private bool CanSee(MessageOfStudent msg) { - if (msg.PlayerState == PlayerState.Quit) + if (msg.PlayerState == PlayerState.Quit || msg.PlayerState == PlayerState.Graduated) return false; if (isSpectatorMode) return true; @@ -703,7 +702,7 @@ namespace Client { foreach (var data in listOfAll) { - StatusBarsOfCircumstance.SetValue(data, gateOpened); + StatusBarsOfCircumstance.SetValue(data, gateOpened, isEmergencyDrawed, isEmergencyOpened); } if (!hasDrawed && mapFlag) DrawMap(); @@ -937,6 +936,19 @@ namespace Client } UpperLayerOfMap.Children.Add(icon); } + foreach (var data in listOfHiddenGate) + { + if (!isEmergencyDrawed) + { + mapPatches[data.X / Preparation.Utility.GameData.numOfPosGridPerCell, data.Y / Preparation.Utility.GameData.numOfPosGridPerCell].Fill = Brushes.LightSalmon; + mapPatches[data.X / Preparation.Utility.GameData.numOfPosGridPerCell, data.Y / Preparation.Utility.GameData.numOfPosGridPerCell].Stroke = Brushes.LightSalmon; + isEmergencyDrawed = true; + } + if (data.Opened && !isEmergencyOpened) + { + isEmergencyOpened = true; + } + } //} ZoomMap(); } @@ -1298,6 +1310,7 @@ namespace Client private List listOfClassroom; private List listOfDoor; private List listOfGate; + private List listOfHiddenGate; private object drawPicLock = new object(); private MessageOfStudent? human = null; private MessageOfTricker? butcher = null; @@ -1363,6 +1376,8 @@ namespace Client ArgumentOptions? options = null; bool gateOpened = false; bool isSpectatorMode = false; + bool isEmergencyOpened = false; + bool isEmergencyDrawed = false; double coolTime0 = -1, coolTime1 = -1, coolTime2 = -1; const double radiusTimes = 1.0 * Preparation.Utility.GameData.characterRadius / Preparation.Utility.GameData.numOfPosGridPerCell; } diff --git a/logic/Client/PlaybackClient.cs b/logic/Client/PlaybackClient.cs index a09a6ed..af923ca 100644 --- a/logic/Client/PlaybackClient.cs +++ b/logic/Client/PlaybackClient.cs @@ -38,7 +38,7 @@ namespace Client public int[,]? ReadDataFromFile(List listOfProp, List listOfHuman, List listOfButcher, List listOfBullet, List listOfBombedBullet, List listOfAll, List listOfChest, List listOfClassroom, - List listOfDoor, List listOfGate, object dataLock) + List listOfDoor, List listOfHiddenGate, List listOfGate, object dataLock) { if (Reader == null) return null; @@ -103,6 +103,7 @@ namespace Client listOfChest.Clear(); listOfClassroom.Clear(); listOfDoor.Clear(); + listOfHiddenGate.Clear(); listOfGate.Clear(); switch (content.GameState) { @@ -146,6 +147,9 @@ namespace Client case MessageOfObj.MessageOfObjOneofCase.GateMessage: listOfGate.Add(obj.GateMessage); break; + case MessageOfObj.MessageOfObjOneofCase.HiddenGateMessage: + listOfHiddenGate.Add(obj.HiddenGateMessage); + break; case MessageOfObj.MessageOfObjOneofCase.MapMessage: break; } @@ -189,6 +193,9 @@ namespace Client case MessageOfObj.MessageOfObjOneofCase.DoorMessage: listOfDoor.Add(obj.DoorMessage); break; + case MessageOfObj.MessageOfObjOneofCase.HiddenGateMessage: + listOfHiddenGate.Add(obj.HiddenGateMessage); + break; case MessageOfObj.MessageOfObjOneofCase.GateMessage: listOfGate.Add(obj.GateMessage); break; @@ -226,6 +233,9 @@ namespace Client case MessageOfObj.MessageOfObjOneofCase.DoorMessage: listOfDoor.Add(obj.DoorMessage); break; + case MessageOfObj.MessageOfObjOneofCase.HiddenGateMessage: + listOfHiddenGate.Add(obj.HiddenGateMessage); + break; case MessageOfObj.MessageOfObjOneofCase.GateMessage: listOfGate.Add(obj.GateMessage); break; diff --git a/logic/Client/StatusBarOfCircumstance.xaml.cs b/logic/Client/StatusBarOfCircumstance.xaml.cs index b4294b6..c00da1f 100644 --- a/logic/Client/StatusBarOfCircumstance.xaml.cs +++ b/logic/Client/StatusBarOfCircumstance.xaml.cs @@ -36,9 +36,8 @@ namespace Client scoresOfStudents.FontSize = scoresOfTrickers.FontSize = fontsize; } - public void SetValue(MessageOfAll obj, bool gateOpened) + public void SetValue(MessageOfAll obj, bool gateOpened, bool hiddenGateRefreshed, bool hiddenGateOpened) { - bool hiddenGateRefreshed = false, hiddenGateOpened = false; time.Text = "Time⏳: " + Convert.ToString(obj.GameTime); status.Text = "📱: " + Convert.ToString(obj.SubjectFinished) + "\n🚪: "; if (gateOpened) @@ -49,14 +48,14 @@ namespace Client { status.Text += "Close\n🆘: "; } - if (obj.SubjectFinished >= Preparation.Utility.GameData.numOfGeneratorRequiredForEmergencyExit) - { - hiddenGateRefreshed = true; - } - if (Preparation.Utility.GameData.numOfStudent == 1 + obj.StudentQuited + obj.StudentGraduated) - { - hiddenGateOpened = true; - } + //if (obj.SubjectFinished >= Preparation.Utility.GameData.numOfGeneratorRequiredForEmergencyExit) + //{ + // hiddenGateRefreshed = true; + //} + //if (Preparation.Utility.GameData.numOfStudent == 1 + obj.StudentQuited + obj.StudentGraduated) + //{ + // hiddenGateOpened = true; + //} if (hiddenGateRefreshed) { if (hiddenGateOpened) diff --git a/logic/cmd/gameServer.cmd b/logic/cmd/gameServer.cmd index aaca460..7df8362 100644 --- a/logic/cmd/gameServer.cmd +++ b/logic/cmd/gameServer.cmd @@ -1,17 +1,25 @@ @echo off -start cmd /k ..\Server\bin\Debug\net6.0\Server.exe --ip 0.0.0.0 --port 8888 --studentCount 2 --trickerCount 1 --gameTimeInSecond 600 --fileName test +start cmd /k ..\Server\bin\Debug\net6.0\Server.exe --ip 0.0.0.0 --port 8888 --studentCount 4 --trickerCount 1 --gameTimeInSecond 600 --fileName test ping -n 2 127.0.0.1 > NUL -start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --port 8888 --characterID 4 --type 2 --occupation 1 +start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --port 8888 --characterID 4 --type 2 --occupation 3 ping -n 2 127.0.0.1 > NUL -start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --port 8888 --characterID 0 --type 1 --occupation 2 +start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --port 8888 --characterID 0 --type 1 --occupation 3 ping -n 2 127.0.0.1 > NUL -start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --port 8888 --characterID 1 --type 1 --occupation 5 +start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --port 8888 --characterID 1 --type 1 --occupation 3 + +ping -n 2 127.0.0.1 > NUL + +start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --port 8888 --characterID 2 --type 1 --occupation 3 + +ping -n 2 127.0.0.1 > NUL + +start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --port 8888 --characterID 3 --type 1 --occupation 3 ping -n 2 127.0.0.1 > NUL \ No newline at end of file diff --git a/logic/cmd/playback.cmd b/logic/cmd/playback.cmd index 018b69a..8bb70ea 100644 --- a/logic/cmd/playback.cmd +++ b/logic/cmd/playback.cmd @@ -1,5 +1,5 @@ @echo off -start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --playbackFile D:\2_autumn\thuai6\THUAI6\logic\cmd\test.thuaipb +start cmd /k ..\Client\bin\Debug\net6.0-windows\Client.exe --cl --playbackFile .\test.thuaipb --playbackSpeed 3 ping -n 2 127.0.0.1 > NUL \ No newline at end of file