Reading file in order to react

Ask gaming related questions (AHK v1.1 and older)
kallera
Posts: 17
Joined: 24 Sep 2017, 20:35

Reading file in order to react

23 Apr 2018, 21:51

Hello folks,


I have stumbled upon the following issue:

The game sends some of it's info to a log file [filename is like 'mychar.log' ] like f.ex. the info that my char's fight is over. That's why I have decided to check this file every time a fight occcurs (info involving my char ofc) to end my automagical fight. Atm. the fight continues for ever if not stopped by the user. Now the question arises how do I stop the fight automagically, how do I check the contents of a file looking backwards from he end of the file (I'm using Windows 10). I have seen the info myself in the log file using a word processor like Notepad and it's command to reread a file? Is the process mayhaps too slow, i don't know that either.

The game adds to the end of the file (atm. the log file is quite big like ~20M) each time a message like 'You have won your fight with [mob name]' or '[mob name] has won the fight. You die'. The same message is btw. displayed also on the screen with floating letters. It does further appear in my chat box tab: system info.

I have not worked with files either on Win10 or with ahk files-commands, so the amount of questions is quite large: how to read from the end of a file with ahk and Win 10, how uch to read, how to compare the file info with some text of my choice, do I have to reserve some memory in the process, would it be wiser and possible to solve the problem in some other way (I don't know where in memory that info is) and so forth and so forth.

Any help (and examples) would be greatly appreciated. Thank you all guys.


--=kallera=--
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Reading file in order to react

27 Apr 2018, 10:43

You need to "Tail" the log.

Luckily for you, I have a Log Tailer class that you can use:

Code: Select all

class CLogTailer {
	CurrentLine := 0
	LastSize := 0
	
	__New(logfile, callback){
		this.callback := callback
		this.logfile := logfile
		this.TimerFn := this.WatchLog.Bind(this)
		this.InitialParse()
		fn := this.TimerFn
		SetTimer, % fn, 250
	}

	; At the start, scan from the END of the log back towards the start...
	; ... and find the callback for each.
	; If the callback returns TRUE, that line matched a regex, so stop...
	; And set this.CurrentLine to that line number, so WatchLog can pick up from that point
	InitialParse(){
		if (FileExist(this.LogFile)){
			numlines := this.CountLines()
			Loop % numlines {
				ln := numlines + 1 - A_Index
				FileReadLine, line, % this.LogFile, % ln
				ret := this.Callback.call(Trim(line))
				if (ret){
					;OutputDebug % "AHK| Found last entry at line " ln " max=" numlines
					FileGetSize, sz , % this.LogFile
					this.LastSize := sz
					this.CurrentLine := numlines
					return
				}
			}
		}
	}
	
	; After the Initial Parse, all subsequent changes in log file size trigger this method.
	; Read all lines that were added since the last tick, and fire the callback for each
	WatchLog(){
		if (FileExist(this.LogFile)){
			FileGetSize, sz , % this.LogFile
			if (sz == this.LastSize){
				return
			}
			if (sz < this.LastSize){
				; Log got smaller - roll over? Reset CurrentLine to 0 and re-parse whole log
				;OutputDebug % "AHK| Log Rollover detected"
				this.CurrentLine := 0
			}
			this.LastSize := sz
			Loop, read, % this.LogFile
			{
				if (A_Index <= this.CurrentLine)
					continue
				;OutputDebug % "AHK| Processing line " A_Index
				this.CurrentLine := A_Index
				if (A_LoopReadLine == "")
					continue
				;OutputDebug % "AHK| Firing callback for line " A_Index
				this.Callback.call(Trim(A_LoopReadLine))
			}
		}
	}
	
	CountLines(){
		c := 0
		Loop, read, % this.LogFile 
		{
			c++
		}
		return c
	}
}
Usage:

Code: Select all

LogTailer := new CLogTailer("C:\myfile.txt", Func("LogChanged"))
return

LogChanged(line){
	; this function is called each time a new line is added to the log.
}
kallera
Posts: 17
Joined: 24 Sep 2017, 20:35

Re: Reading file in order to react

27 Apr 2018, 23:25

You are a truly Evil man, EvilC. Thanks for your helpful reply.

Do I only have to add all that code that you have written somewhere in my main file? What if (one of) the text(s) I'm expecting, let's say the text 'Belzebub', is found? Or the text 'You die''? I would like to react on those messages by say like stopping my attack loop (means releasing my NumLock key)? Or do I have to add (all) the text(s) I'm looking for (like 'Belzebub', 'You die' etc to the LogChanged(line) function?

Please clarify a bit. I'm only a newbie at coding anyway, so what may be selfevident to you is not at all easily understood to me.

Thank you very much for sharing your awesome work anyway.

--=kallera=--
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Reading file in order to react

28 Apr 2018, 13:10

Best to save the first snippet (The CLogTailer class) as CLogTailer.ahk and then use #include CLogTailer.ahk at the start of your script.
Looking at that class, it's not the one I thought it was. It should work, but it's not very efficient. I will see if I can find the better code that I should have somewhere.
Syntax using it would be the same tho, so you can just start writing code using CLogTailer and we can drop in a better version later if I can find the code.
kallera
Posts: 17
Joined: 24 Sep 2017, 20:35

Re: Reading file in order to react

30 Apr 2018, 09:56

Awesome, thank you, will do that as soon as possible, perhaps already tomorrow. At the same time I will (atm only planning to) backup the log file and take a backup of it. The new logfile will of course be considerably shorter.

--=kallera=--
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Reading file in order to react

30 Apr 2018, 11:46

I had a look around, but I could not find the more efficient log tailer class that I thought I wrote.
So I sat down and wrote it :P

Code: Select all

#SingleInstance force
#Persistent

lt := new CLogTailer("myfile.txt", Func("NewLine"))
return

NewLine(text){
	ToolTip % "New line added @ " A_TickCount ": " text
}

class CLogTailer {
	__New(logfile, callback){
		this.file := FileOpen(logfile, "r-d")
		this.callback := callback
		; Move seek to end of file
		this.file.Seek(0, 2)
		fn := this.WatchLog.Bind(this)
		SetTimer, % fn, 100
	}
	
	WatchLog(){
		Loop {
			p := this.file.Tell()
			l := this.file.Length
			line := this.file.ReadLine(), "`r`n"
			len := StrLen(line)
			if (len){
				RegExMatch(line, "[\r\n]+", matches)
				if (line == matches)
					continue
				this.callback.Call(Trim(line, "`r`n"))
			}
		} until (p == l)
	}
}
kallera
Posts: 17
Joined: 24 Sep 2017, 20:35

Re: Reading file in order to react

02 May 2018, 04:36

Thank you once again for your kind efforts, EvilC.

It seems like you have in some way that remains a mystery to me, have added a class (I believe) with c / c++ code? Still the code uses AHK-code ' := ' and, afaik c-code '==' ? How come both?

I must however ask once more for help with the usage. Where do i put in the text I am seaching for (like the text 'belzebub' or the text 'You die. Your death is very painful.' or anything)?

Where do I put in the commands I wish that my code should do in case a match is found (like Send {Lcontrol down} SendInput,{Numpad7} Send {Lcontrol up}) etc etc?

Where do I put in the filename to my log file: '(full path?) mychar_name.log'?

I'm sorry to be so ignorant, but that is because I am, I can't help it either since I was born this way and slept thru school:)


Rgds
--=kallera=--
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Reading file in order to react

02 May 2018, 07:47

Nope, it's pure AHK. In AHK, == is a valid comparison operator - = in AHK is sometimes comparison, sometimes assignment, so I avoid using it like the plague.
The only valid use of = for comparison in AHK is to do a case-insensitive string comparison. In all other cases, == is preferable as it is unambiguous.

As each new line is added to the log, the function NewLine() is called, and the new line of text will be in the text variable.
Inspect the contents of the text variable and take action as appropriate. For example:

Code: Select all

NewLine(text){
	if (text == "You die. Your death is very painful."){
		msgbox YOU DIED
	}
}
kallera
Posts: 17
Joined: 24 Sep 2017, 20:35

Re: Reading file in order to react

04 Aug 2018, 12:44

@EvilC:

To our discussion about reaction to new input to my logfile (btw. the logfile is ”D:\Main\RyzomII\Ryzom\ryzom_live\save\saija_log.txt”). I have tried to do (in different variations) do as you said however without success.

I have added to my main file this sequence as a separate file (called clogtailer.ahk) as you said:

Code: Select all

;from Evilc
#SingleInstance force
#Persistent

lt := new CLogTailer("myfile.txt", Func("NewLine"))
return

NewLine(text){
	ToolTip % "New line added @ " A_TickCount ": " text
}

class CLogTailer {
	__New(logfile, callback){
		this.file := FileOpen(logfile, "r-d")
		this.callback := callback
		; Move seek to end of file
		this.file.Seek(0, 2)
		fn := this.WatchLog.Bind(this)
		SetTimer, % fn, 100
	}
	
	WatchLog(){
		Loop {
			p := this.file.Tell()
			l := this.file.Length
			line := this.file.ReadLine(), "`r`n"
			len := StrLen(line)
			if (len){
				RegExMatch(line, "[\r\n]+", matches)
				if (line == matches)
					continue
				this.callback.Call(Trim(line, "`r`n"))
			}
		} until (p == l)
	}
}
I have also added these lines (separately, not reachable by the rest of the code) to my main file:

Code: Select all

; from EvilC
LogTailer := new CLogTailer("C:\myfile.txt", Func("LogChanged"))
return
LogChanged(line){
	; this function is called each time a new line is added to the log.
}

}
return
This is correct, right? Or shall this (last) code be separately called?

Now I still need to know where I can put a line containing the path to the logfile (”D:\Main\RyzomII\Ryzom\ryzom_live\save\saija_log.txt”) in the code. Further, where can I put the compare code, f.ex

(IF (text == belzebub) ...various code),
(IF (text == you die) ...other commands ) etc.

I’m sorry insisting but I really need to know. I would not return to the matter after several months if I wouldn’t. Please help me.

Rgds.
--=kallera=--
ajobbins
Posts: 3
Joined: 22 Sep 2020, 09:25

Re: Reading file in order to react

01 Mar 2021, 20:33

Thanks @evilC for this example, I have it working and it's doing mostly what I want

I'm using the revised example you posted to tails the logs, but unlike your original example, it does not scan the log backwards to find the last instance first. Is there a more efficient way to achieving that, as I need to find the last example of a match in the log when the script initially loads, then just tail the log from that point - or should I just use the original example posted?
ajobbins
Posts: 3
Joined: 22 Sep 2020, 09:25

Re: Reading file in order to react

02 Mar 2021, 01:29

Further to the above - I have tried using the first example code to read backwards through the file. It's doing that, and finding matches, but then it doesn't stop. It just keeps working it's way backwards through the log file, finding matches to my regex until it reaches the start of the file. Any hints as to how I can get it to stop after the first match (working backwards)?
ajobbins
Posts: 3
Joined: 22 Sep 2020, 09:25

Re: Reading file in order to react

02 Mar 2021, 08:27

Thanks so much for replying, really appreciate it.

Do I understand correctly that modifying the last line (before closing brackets from return to return, True will stop the function continuing to loop past the first hit?

Code: Select all

	InitialParse(){
		if (FileExist(this.LogFile)){
			numlines := this.CountLines()
			Loop % numlines {
				ln := numlines + 1 - A_Index
				FileReadLine, line, % this.LogFile, % ln
				ret := this.Callback.call(Trim(line))
				if (ret){
					;OutputDebug % "AHK| Found last entry at line " ln " max=" numlines
					FileGetSize, sz , % this.LogFile
					this.LastSize := sz
					this.CurrentLine := numlines
					return, True
				}
			}
		}
	}
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Reading file in order to react

02 Mar 2021, 12:37

No, you misunderstand. I was meaning that you do something like this

Code: Select all

lt := new CLogTailer("myfile.txt", Func("NewLine"))
return

NewLine(text){
	if (I_find_what_I_want_from_the_log_and_wish_to_stop_the_initial_parse){
		return true
	}
}
However, upon looking at my code, your NewLine function would not know whether it was being called as part of the initial parse, or as a result of new lines being added to the log.

So I would modify my library thus:

change this.InitialParse() to:

Code: Select all

		this.DoingInitialParse := 1
		this.InitialParse()
		this.DoingInitialParse := 0
Then use something like this for your function:

Code: Select all

lt := new CLogTailer("myfile.txt", Func("NewLine"))
return

NewLine(text){
	global lt
	if (lt.DoingInitialParse){
		; If we hit this code block, then we know this function is being called during the initial parse
		if (I_find_what_I_want_from_the_log_and_wish_to_stop_the_initial_parse){
			return true
		}
	} else {
		; this is the code that will be used for all other times a line is added to the log
	}
}
rht5692
Posts: 2
Joined: 18 Oct 2021, 11:46

Re: Reading file in order to react

03 Nov 2021, 03:56

Hi EvilC,

A big thank you for posting all your code about this subject

Could you explain where I have to put the new lines of your last post in the library ?

thank you very much for your help

Return to “Gaming Help (v1)”

Who is online

Users browsing this forum: No registered users and 99 guests