@@ -20,6 +20,11 @@ export interface Options {
2020 logFile : string ;
2121 configFile : string ;
2222
23+ /**
24+ * Path to save any log files
25+ */
26+ logDirectory ?: string ;
27+
2328 /**
2429 * The number of lines in each parsing group.
2530 */
@@ -71,6 +76,9 @@ export class LogWatcher extends HspEventsEmitterClass {
7176
7277 private _updateDebouncer : ( filePath : string , stats : fs . Stats ) => void ;
7378
79+ private _logStream : fs . WriteStream | null = null ;
80+ private _linesQueued = new Array < string > ( ) ;
81+
7482 constructor ( options ?: Partial < Options > ) {
7583 super ( ) ;
7684
@@ -89,6 +97,11 @@ export class LogWatcher extends HspEventsEmitterClass {
8997 throw new Error ( 'Log file path does not exist.' ) ;
9098 }
9199
100+ if ( this . options . logDirectory ) {
101+ log ( 'output log directory: %s' , this . options . logDirectory ) ;
102+ fs . mkdirSync ( this . options . logDirectory , { recursive : true } ) ;
103+ }
104+
92105 // Copy local config file to the correct location.
93106 // We're just gonna do this every time.
94107 const localConfigFile = path . join ( __dirname , '../log.config' ) ;
@@ -173,6 +186,8 @@ export class LogWatcher extends HspEventsEmitterClass {
173186 this . emit ( 'gamestate-changed' , gameState ) ;
174187 updated = false ;
175188 }
189+
190+ this . _handleLogging ( line , gameState ) ;
176191 } ) ;
177192
178193 if ( updated ) {
@@ -182,6 +197,55 @@ export class LogWatcher extends HspEventsEmitterClass {
182197 return gameState ;
183198 }
184199
200+ /**
201+ * Internal method to potentially write a line to the output log (if enabled).
202+ * @param line
203+ * @param gameState
204+ */
205+ private _handleLogging ( line : string , gameState : GameState ) {
206+ const activeOrComplete = gameState . active || gameState . complete ;
207+ if ( ! this . options . logDirectory || ! activeOrComplete ) {
208+ return ;
209+ }
210+
211+ // If there's no file stream and we have enough info to create one, then create one.
212+ if ( ! this . _logStream && gameState . active && gameState . numPlayers === 2 ) {
213+ const [ player1 , player2 ] = gameState . getAllPlayers ( ) ;
214+ const ext = path . extname ( this . options . logFile ) ;
215+ const filename = `${ gameState . startTime } _${ player1 ?. name ?? 'unknown' } _vs_${ player2 ?. name ?? 'unknown' } ${ ext } ` ;
216+
217+ // Convert the name to something safe to save
218+ const specialChars = String . raw `<>:"/\|?*` . split ( '' ) ;
219+ const filenameSlugged = filename . split ( '' ) . map ( c => specialChars . includes ( c ) ? '!' : c ) . join ( '' ) ;
220+
221+ // Create write stream
222+ const filepath = path . join ( this . options . logDirectory , filenameSlugged ) ;
223+ this . _logStream = fs . createWriteStream ( path . normalize ( filepath ) ) ;
224+
225+ // Flush our "buffer"
226+ this . _logStream . write ( this . _linesQueued . join ( '' ) ) ;
227+ this . _linesQueued = [ ] ;
228+ }
229+
230+ // Write to output log.
231+ // If we are still waiting for players to load,write to a buffer beforehand.
232+ // This is because the filename is decided AFTER the file has started.
233+ if ( gameState . active || ( gameState . complete && this . _logStream ) ) {
234+ if ( this . _logStream ) {
235+ this . _logStream . write ( line + '\n' ) ;
236+ } else {
237+ // No file stream, so write to our "buffer"
238+ this . _linesQueued . push ( line + '\n' ) ;
239+ }
240+ }
241+
242+ // If the game is complete, close the output stream
243+ if ( gameState . complete ) {
244+ this . _logStream ?. end ( ) ;
245+ this . _logStream = null ;
246+ }
247+ }
248+
185249 private _update ( filePath : string , stats : fs . Stats ) : void {
186250 // We're only going to read the portion of the file that we have not read so far.
187251 const newFileSize = stats . size ;
0 commit comments