[TOOL] Sequence

Talk about creating Grimrock 1 levels and mods here. Warning: forum contains spoilers!
Post Reply
Marble Mouth
Posts: 52
Joined: Sun Feb 10, 2013 12:46 am
Location: Dorchester, MA, USA

[TOOL] Sequence

Post by Marble Mouth »

Hi everyone. I wanted to share something that I made. I call it "sequence". This really doesn't introduce any new functionality, but using it has streamlined a lot of the things that I commonly do in the editor. It is infrastructure code that allows you to define a sequence of events with delays in between. There are a couple of auxiliary functions provided as well. It is available through my dropbox.
SpoilerShow
I really do recommend downloading the .zip file with documentation. For convenience, here is the business end of the code:

Code: Select all

synchInterval = 0.1
--[[All delays should be divisible by synchInterval.
e.g. if synchInterval is 0.1 then acceptable
delays are 0.1 , 0.2 , ... , 1.0 , 1.1 , ...
Lower interval -> more precise timing & more cycles
consumed synching. Adjust to your taste.
--]]

newSequence = function( copyFromSequence )

	initSynch()

	local s = {
        copy = newSequence,
		--[[I kind of hate the syntax below. At first glance,
		it looks as if it's doing nothing, but it's absolutely
		necessary for the way this table works.
		--]]
		addEvent = addEvent,
		execute = execute,
		abort = abort,
		setInterruptMode = setInterruptMode,
	}

	if copyFromSequence then
		--[[There is a danger here: upvalues.
		Grimrock does not currently (US 3/9/2013) support
		upvalues, but I remain hopeful that it will eventually.
		If we simply copy the function as below,
		then we are actually referencing the same closure.
		I don't see any way around this. I'm afraid that
		this behavior will be an unpleasant surprise for
		some users.
		--]]
		s.eventCount = copyFromSequence.eventCount
		for i , v in ipairs(copyFromSequence) do
			s[i] = v
		end
		s.interruptMode = copyFromSequence.interruptMode
	else
		s.eventCount = 0
	end

	return s
end

addEvent = function( sequence , newEvent )
	local i = sequence.eventCount + 1
	sequence.eventCount = i
	sequence[i] = newEvent
end

execute = function( sequence , extraArgument )
    if ( not sequence.currentEvent ) or ( sequence.interruptMode == "restart" ) then
        sequence.extra = extraArgument
        sequence.currentEvent = 1
        sequence.tickTarget = nil
        sequence.timerId = nil
        executeEvent( sequence )
    elseif sequence.interruptMode == "parallel" then
        sequence:copy():execute( extraArgument )
    end
end

abort = function( sequence )
    sequence.currentEvent = nil
    sequence.tickTarget = nil
    sequence.timerId = nil
end

setInterruptMode = function( sequence , mode )
--mode should be one of "ignore" , "restart" , or "parallel"
--default is "ignore"
    sequence.interruptMode = mode
end

------------------------------------------
--[[Functions below this line are intended
for internal use only.
--]]

initSynch = function()
	if synchInitialized then
		return
	end

	synchTimers()
	synchInitialized = true
end

timers = {}
--Keys are timer id's. Values are sequences.

synchTimers = function( synch )
--ensure that all timers are on the same level as the party
	if ( not synch ) or
		( not ( synch.level == party.level ) ) then

		if synch then
			synch:destroy()
		end

		synch = spawn( "timer" , party.level , 0 , 0 , 0 )
		synch:setTimerInterval( synchInterval )
		synch:addConnector( "activate" , "sequenceScript" , "synchTimers" )
		synch:activate()

		local newTimers = {}
		for k , v in pairs( timers ) do
            local timer = findEntity( k )
            timer:destroy()
            if v.tickTarget then
                local newTimer = spawn( "timer" , party.level, 0 , 0 , 0 )
                v.timerId = newTimer.id
                newTimer:setTimerInterval( synchInterval )
                newTimer:addConnector( "activate" , "sequenceScript" , v.tickTarget )
                newTimer:activate()
                newTimers[newTimer.id] = v
            else
                v.timerId = nil
            end
		end
		timers = newTimers

	end
end

executeEvent = function( sequence )
--this function is called to begin an event

    if not sequence.currentEvent then
        --aborted
        return
    end

    local event = sequence[sequence.currentEvent]

    if not event then
        --reached the end of the sequence
        sequence.currentEvent = nil
        return
    end

	if event.delayBefore then
        sequence.tickTarget = "delayBeforeTick"
        sequence.beforeTicks = nil
		local timer = spawn( "timer" , party.level , 0 , 0 , 0 )
		sequence.timerId = timer.id
		timer:setTimerInterval( synchInterval )
		timer:addConnector( "activate" , "sequenceScript" , sequence.tickTarget )
		timers[timer.id] = sequence
		timer:activate()
	else
        runEventFunction( sequence )
	end
end

delayBeforeTick = function( timer )
    local sequence = timers[timer.id]

    if not ( sequence.timerId == timer.id ) then
        --aborted or restarted
        timers[timer.id] = nil
        timer:destroy()
        return
    end

    sequence.beforeTicks = ( sequence.beforeTicks or 0 ) + 1
    if ( sequence.beforeTicks * synchInterval ) >= sequence[sequence.currentEvent].delayBefore then
        sequence.tickTarget = nil
        sequence.beforeTicks = nil
        sequence.timerId = nil
        timers[timer.id] = nil
        timer:destroy()
        runEventFunction( sequence )
    end
end

delayAfterTick = function( timer )
    local sequence = timers[timer.id]

    if not ( sequence.timerId == timer.id ) then
        --aborted or restarted
        timers[timer.id] = nil
        timer:destroy()
        return
    end

    sequence.afterTicks = ( sequence.afterTicks or 0 ) + 1
    if ( sequence.afterTicks * synchInterval ) >= sequence[sequence.currentEvent].delayAfter then
        sequence.tickTarget = nil
        sequence.afterTicks = nil
        sequence.timerId = nil
        timers[timer.id] = nil
        timer:destroy()
        sequence.currentEvent = sequence.currentEvent + 1
        executeEvent( sequence )
    end
end

runEventFunction = function( sequence )
--this function is called to run the actual event code
--and determine what to do immediately thereafter

    if not sequence.currentEvent then
        --aborted
        return
    end

    local e = sequence[sequence.currentEvent]
    e.event( sequence.extra )
    if e.delayAfter then
        sequence.tickTarget = "delayAfterTick"
        sequence.afterTicks = nil
        local timer = spawn( "timer" , party.level , 0 , 0 , 0 )
        sequence.timerId = timer.id
		timer:setTimerInterval( synchInterval )
		timer:addConnector( "activate" , "sequenceScript" , sequence.tickTarget )
		timers[timer.id] = sequence
		timer:activate()
    else
        sequence.currentEvent = sequence.currentEvent + 1
        executeEvent( sequence )
    end
end
Here is an example usage:

Code: Select all

function onButton()
	mySequence:execute()
end

mySequence = newSequence()
mySequence:addEvent{
	event = function()
		hudPrint("The air behind the gate appears to be congealing")
		--maybe add some fx here?
	end,
	delayAfter = 3,
}
mySequence:addEvent{
	event = function()
		spawn( "snail" , 1 , 15 , 15 , 0 )
	end,
	delayAfter = 1,
}
mySequence:addEvent{
	event = function()
		hudPrint("Where did that snail come from?")
	end,
	delayAfter = 0.5,
}
mySequence:addEvent{
	event = function()
		gate_1:open()
	end,
}
If you encounter any bugs or want any other functionality implemented, please don't hesitate to say so in this thread. If you have suggested code changes, that's even better :D
User avatar
Damonya
Posts: 134
Joined: Thu Feb 28, 2013 1:16 pm
Location: France
Contact:

Re: [TOOL] Sequence

Post by Damonya »

I tested and it's interesting. It greatly facilitates the tasks without using unnecessary timer. Thanks for this tool.
Orwak - MOD - Beta version 1.0
User avatar
Damonya
Posts: 134
Joined: Thu Feb 28, 2013 1:16 pm
Location: France
Contact:

Re: [TOOL] Sequence

Post by Damonya »

I come back months after, but in fact there is a big problem with your script. This works in the editor and if you make a new game, but if you are unlucky enough to save the game and reload the save, when you execute the script, you have a "crash to desktop" with this error.log:

Code: Select all

#sequenceScript:59: attempt to call global 'executeEvent' (a nil value)
stack traceback:
	#sequenceScript:59: in function 'execute'
	#script_entity_1:32: in function <#script_entity_1:31>
	[string "ScriptEntity.lua"]: in function 'onMessageReceived'
	[string "MessageSystem.lua"]: in function 'sendMessageToEntity'
	[string "MessageSystem.lua"]: in function 'broadcastMessage'
	[string "TriggerEvents.lua"]: in function 'fire'
	[string "PressurePlate.lua"]: in function 'pressed'
	[string "PressurePlate.lua"]: in function 'updateState'
	[string "PressurePlate.lua"]: in main chunk
	[string "MessageSystem.lua"]: in function 'sendMessage'
	[string "Map.lua"]: in function 'moveEntity'
	[string "Party.lua"]: in function 'setPosition'
	[string "Party.lua"]: in function 'update'
	[string "Party.lua"]: in function 'update'
	[string "Map.lua"]: in function 'updateEntities'
	[string "Dungeon.lua"]: in function 'updateLevels'
	[string "GameMode.lua"]: in function 'update'
	[string "Grimrock.lua"]: in function 'display'
	[string "Grimrock.lua"]: in main chunk
I've realized that far too late in my mod, therefore, in order to identify the problem I built a small mod with only your script, taking your example. And the bug is produced when a save is loaded.

Mod :http://www.meteobell.com/Divers/JDR/Gri ... quence.dat
Source : http://www.meteobell.com/Divers/JDR/Gri ... quence.zip

Thank you for the help.

EDIT : Personally I bypassed the problem on my mod, using timers instead of your script
Orwak - MOD - Beta version 1.0
Post Reply