[script] Extended timer (setConstant, tickLimit, callbacks)

Talk about creating Grimrock 1 levels and mods here. Warning: forum contains spoilers!
User avatar
JKos
Posts: 464
Joined: Wed Sep 12, 2012 10:03 pm
Location: Finland
Contact:

[script] Extended timer (setConstant, tickLimit, callbacks)

Post by JKos »

Hello,

Timers are pretty awkward to use so I made a script which extends the timer functionality. It was much harder to implement than I thought it would be. There was all kind of weird problems, like if you destroy the timer in connector it will still call all remaining connectors but the timer object passed to connector event function is corrupted in some way (bad object error), that was pain in the ass to debug...but finally it works. But at least now I know everything about the timers :D

You can use it like normal timer but you have to spawn it by calling the timers:create(id) function
Example
Get the sources from the bottom of this post, create a script entity named myscript and follow the examples.

Code: Select all

function createTimer()
   local mytimer = timers:create('mytimer_id')
   mytimer:addConnector('activate','myscript','tick')
   mytimer:setTimerInterval(1)
   mytimer:activate()
end
function tick(timer)
   print(timer.id..': tick')
end
you have to call myscript.createTimer from somewhere. Extended timers cant be created in initialize time, they must be created inside of some function.
Difference to native connectors is that the connector even function will have the extended timer object as an argument.

Callbacks (with arguments)

Timer:addCallback(callbackFunction,callbackArguments)
Arguments must be passed in table format

Code: Select all

mytimer:addCallback(
    function(self,param1)  
        print(self.id..': Hello  '..param1)
    end,
   {'You'}
)
Prints mytimer: Hello You on every tick
Callbacks will have the extended timer object as a first argument.

tickLimit
Timer:setTickLimit(count,destroyFlag)
timer will be deactivated after (count) rounds, if destroyFlag is true it will be also destroyed.

Code: Select all

mytimer:setTickLimit(5)
setConstant
Timer will stay in real time all the time. Technically it spawns a timer to all dungeon levels which calls same functions than the original timer. Only the timer on same level as the party is active.
This was the main purpose of this script but I added some other features too.
You have to tell the amount of the dungeon levels to timers-script. Just call the timers:setLevels(amount) before you call the setConstant-function. You need to call it only once, all timers will use the same value.

Code: Select all

timers:setLevels(4)
mytimer:setConstant()


find timer
you can't access the exteded timer by findEntity or by typing the id in script like normal timers, it will return the actual(native) timer entity which is wrapped inside the extended timer. So you have to use
timers:find(timer_id) function

Code: Select all

local mytimer = timers:find('mytimer_id')
Full example

Code: Select all

function createTimer()
  timers:setLevels(5) -- modify this to the amount of levels in your dungeon.
   local mytimer = timers:create('mytimer_id')
   mytimer:addConnector('activate','myscript','tick')
   mytimer:setTimerInterval(3)
   mytimer:setTickLimit(5)
  mytimer:addCallback(
      function(self,param1)  
          print(self.id..': Hello  '..param1)
      end,
     {'You'}
  )
   mytimer:setConstant()
   mytimer:activate()
   local mytimer2 = timers:find('mytimer_id')
   print(mytimer2.id)
end
function tick(timer)
   print(timer.id..': tick')
end



Source
Create a new script entity named timers and copy paste this script in it.
I have tested that it works in game too and is save game compatible.

Code: Select all

objects = {}
debug = false
settings = {levels = 0}

-- spawn a new timer
function create(self,id,plevel)
 plevel = plevel or party.level
 local timerEntity = spawn('timer',plevel,0,0,1,id)
 self.objects[id] = wrap(timerEntity)
 timerEntity:addConnector('activate','timers','callCallbacks')
 return self.objects[id]
end

function find(self,id)
	return self.objects[id]
end


function setLevels(self,levels)
	self.settings.levels = levels
end

-- create a wrapper object to timer passed as argument
function wrap(timer)
 local wrapper = {
    id = timer.id,
	level = timer.level,
    interval = 0,
    connectors = {},
	active = false,
	callbacks = {},
	tick = 0,
	addConnector = function(self,paction,ptarget,pevent)
		self:addCallback(
			function(self,scriptId,functionName) 
				findEntity(scriptId)[functionName](self)
			end,
			{ptarget,pevent}
		)
	end,
	activate = function(self)
	    self.active = true
		if self.isConstant then
			timers.objects[self.id..'_'..party.level]:activate()
		else
			findEntity(self.id):activate()
		end
		if self.instant then
			
			self:callCallbacks()
		end
	end,
	deactivate = function(self)
	    self.active = false
		findEntity(self.id):deactivate()
		if (self.isConstant) then
			for l=1, timers.settings.levels do
				timers.objects[self.id..'_'..l]:deactivate()
			end
		end		
	end,
	toggle = function(self)
		if (self.active) then 
			self:deactivate()
		else
			self:activate()
		end
	end,	
	isActivated = function(self)
		return self.active
	end,		
	
	-- If set the first function calls will be instant after the activation.
	setInstant = function(self,bool)
		self.instant = bool
	end,
	
	setTimerInterval = function(self,interval)
	    self.interval = interval
		findEntity(self.id):setTimerInterval(interval)
	end,
	setConstant = function(self)
		if timers.settings.levels == 0 then
			print('You must set the amount of dungeon levels. For example: timers:setlevels(5)')
			return
		end
		self.isConstant = true
		timers.copyTimerToAllLevels(self)
	end,
	destroy = function(self)
		findEntity(self.id):destroy()
		timers.objects[self.id] = nil
		if (self.isConstant) then
			for l=1,timers.settings.levels do
				timers.objects[self.id..'_'..l]:destroy()
			end
		end
	end,	
	addCallback = function(self,callback,callbackArgs)
		callbackArgs = callbackArgs or {}
		self.callbacks[#self.callbacks+1] = {callback,callbackArgs}
		
	end,
	callCallbacks = timers.callCallbacks,
	setTickLimit = function(self,limit,destroy)
		self:addCallback(
			function(self,limit,destroy)
				
				if timers.debug then 
					print('tick count:'..self.tick) 
				end
				if self.tick >= limit then
					self:deactivate()
					if timers.debug then print('timer '..self.id..' deactivated: tick limit '..limit) end
					if (destroy) then
						-- mark as destroyed
						self.destroyed = true
					end
				end
			end,
			{limit,destroy}
		)
	end	
 }
 return wrapper
end

function callCallbacks(timerEntity)
	--print(timerEntity.id..' callbacks called')
	local extTimer = objects[timerEntity.id]
	extTimer.tick = extTimer.tick + 1
	
	for _,callback in ipairs(extTimer.callbacks) do
		callback[1](extTimer,unpack(callback[2]))
	end
	
	if (extTimer.destroyed) then
		if timers.debug then print('timer '..extTimer.id..' destroyed') end
		extTimer:destroy()
	end
end

function copyTimerToAllLevels(self)

		for l=1,timers.settings.levels do
		
			local t = timers:create(self.id..'_'..l,l)
			
			-- if interval is larger tha 1 second
			-- use 1 second interval and count to actual interval
			-- this way the gap between level changes should stay minimal
			-- Thanks to Betty for the idea 
			if self.interval > 1 then
				t:setTimerInterval(1)
				self.count = 0
				t:addCallback(
					function(self,timer_id,interval) 					
						local timer = timers.objects[timer_id]
						if (self.level == party.level) then
							timer.count = timer.count + 1
						else
							self:deactivate()
							timers:find(timer_id..'_'..party.level):activate()
						end
						if (timer.count == interval) then
							timer:callCallbacks()
							timer.count = 0
						end
		
					end,
					{self.id,self.interval}
				)
			else
				t:setTimerInterval(self.interval)
				t:addCallback(
					function(self,timer_id) 					
						local timer = timers.objects[timer_id]
						
						if (self.level == party.level) then
							
							timer:callCallbacks()
						else
							self:deactivate()
							timers:find(timer_id..'_'..party.level):activate()
						end
					end,
					{self.id}
				)
			end
					
		end	
		self:deactivate()
end
Last edited by JKos on Sun Nov 25, 2012 10:10 pm, edited 2 times in total.
- LoG Framework 2http://sites.google.com/site/jkoslog2 Define hooks in runtime by entity.name or entity.id + multiple hooks support.
- cloneObject viewtopic.php?f=22&t=8450
User avatar
Komag
Posts: 3659
Joined: Sat Jul 28, 2012 4:55 pm
Location: Boston, USA

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by Komag »

wow, that's heavy powerful stuff! Thanks for this, I'm sure it will get some good use. wondering, did you add an estate tick for the follow party when changing levels? there are two considerations:

1 - on the next tick it discovers it's not on the same level, so destroys and recreates, starts, and takes one additional second (if it's a 1s timer) before reaching the next tick to up the count. this may not be an issue if the tick count is raised before destroying.

2 - when the party changes level, the timer will run at half speed until it detects it's no longer on the party level and destructs and respawns. if it's a 1s tick then instead of the level transition being from near-zero to 1, it is near-zero to 2, for an average of one extra second. So you may need to add an extant tick count for this as well.

The effect of this can more strongly felt if the time interval is large, and can be felt extremely so if the player changes more than one level, such as being teleported four floors down as in the original game fighters challenge, in which case the timer back on level 7 was running something like 1/10 speed and took forever to realize the party was gone!

These reasons give a strong push to go with something more like what petri suggests, which is to place a timer on EVERY level and only have the timer which is on the same level have effect. That way, whenever the party arrives on a new level, the existing timer on that level is already working, ticking full speed, no delays, no time dilation, no problem!
Finished Dungeons - complete mods to play
User avatar
JKos
Posts: 464
Joined: Wed Sep 12, 2012 10:03 pm
Location: Finland
Contact:

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by JKos »

Damn, you are right about the 2nd problem, 1st one is ok if I understood correctly what you meant, level check is always called last (you can't add any connectors or callbacks after followParty()-call).
About the 2nd problem:Yes, there is a gap when you change the level, and it's noticeable with large intervals. So you just have to know that when you use it. The gap could be reduced by adding a party:onMove hook which recreates all timers which are set to follow the party when the level is changed, but even it can't solve the "teleport 7 levels" problem.

Anyway this solution is good enough for the issue I developed it for, but thanks for pointing out this problem.
- LoG Framework 2http://sites.google.com/site/jkoslog2 Define hooks in runtime by entity.name or entity.id + multiple hooks support.
- cloneObject viewtopic.php?f=22&t=8450
User avatar
Komag
Posts: 3659
Joined: Sat Jul 28, 2012 4:55 pm
Location: Boston, USA

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by Komag »

Yeah, most dungeons probably wouldn't have that issue, but it's good at least to be aware. Would it be possible to implement something like the timer-on-every-level approach?
Finished Dungeons - complete mods to play
User avatar
JKos
Posts: 464
Joined: Wed Sep 12, 2012 10:03 pm
Location: Finland
Contact:

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by JKos »

I made a script which spawns cloned timers to all levels, but it doesn't work either because the timer on target level runs slower before I move to that level -> target level timer will start ticking from earlier point.
I'm not gonna spend any more time to this :) I just hope Petri will catch my Timer:setConstant() request from the feature request thread.
- LoG Framework 2http://sites.google.com/site/jkoslog2 Define hooks in runtime by entity.name or entity.id + multiple hooks support.
- cloneObject viewtopic.php?f=22&t=8450
User avatar
Komag
Posts: 3659
Joined: Sat Jul 28, 2012 4:55 pm
Location: Boston, USA

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by Komag »

It's not that they need to be in sync, it's that the same-level timer will take over where the other left off ticking the same function, that's all.

viewtopic.php?p=41255#p41255
Finished Dungeons - complete mods to play
User avatar
JKos
Posts: 464
Joined: Wed Sep 12, 2012 10:03 pm
Location: Finland
Contact:

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by JKos »

Yeah I did do just that (I read that post), and it doesn't work. The next tick (triggered by timer on level where party is moved) after the level change will come later because the target level timer is late in time.
Example:
Source level timer will point in 5 seconds
Target level timer points in 2,5 seconds.

So when I move to the target level, it will start from 2,5 seconds point instead of 5 seconds.
- LoG Framework 2http://sites.google.com/site/jkoslog2 Define hooks in runtime by entity.name or entity.id + multiple hooks support.
- cloneObject viewtopic.php?f=22&t=8450
User avatar
Komag
Posts: 3659
Joined: Sat Jul 28, 2012 4:55 pm
Location: Boston, USA

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by Komag »

I think I see what you're saying. Do you mean like this:

Floor 1, timer starts running for 100 seconds
Floor 2, timer starts running for 100 seconds, but goes 1/2 speed

Player gets to stairs and walks down to floor two when floor 1 timer is at 90 seconds. So after 10 more seconds, timer should trigger.
But when player walked down stairs, timer on floor 2 was only at 45 seconds because it was going slow, and now it will run full speed but still takes 55 seconds to finally trigger.

I see that would be a problem for long timers. So maybe for short timers, like 1 second, it's not an issue.

Hmm, I hope you're right that we get Timer:setConstant(), that would definitely help a lot with this issue! ;)
Finished Dungeons - complete mods to play
User avatar
JKos
Posts: 464
Joined: Wed Sep 12, 2012 10:03 pm
Location: Finland
Contact:

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by JKos »

You got that right. But this script works fine for short intervals if you don't teleport multiple levels.
- LoG Framework 2http://sites.google.com/site/jkoslog2 Define hooks in runtime by entity.name or entity.id + multiple hooks support.
- cloneObject viewtopic.php?f=22&t=8450
Batty
Posts: 509
Joined: Sun Apr 15, 2012 7:04 pm

Re: [script] Extended timer (followParty, tickLimit, callbac

Post by Batty »

For longer time periods couldn't you do:

Code: Select all

x = 0
function ticktock(timer)
   if party.level == timer.level then
      x = x + 1
   end
   if x == 100 then
      do this
      all timers:deactivate()
   end
end
The timer on each level calling this function would be set to one second then you don't have the long interval problem.
Post Reply