• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

[TFS 1.3] seasons of the year (async)

zbizu

Legendary OT User
Joined
Nov 22, 2010
Messages
3,323
Solutions
26
Reaction score
2,690
Location
Poland
In preparation to version 12, I've wrote a code that could be used for feyrist day/night cycle or for any server to create alternative versions of specific areas.

What it does:
  • It transforms a large area into another biome (you have to fill biomes table manually).
  • It does that in delays so the server doesn't lag during the processing.
  • It's a mod. This means that both files will go to data/scripts folder.

video: i.imgur.com/HX0TqjC.mp4

how to use:
I recommend writing a globalevent yourself. Either onStartup (os.date(...) for seasons) or onThink (getWorldTime() for day/night cycle)

the code:

asyncArea.lua:
Code:
local defaultChunkSize = 16
local defaultDelay = 200

Area = setmetatable ({ },
    {
        __call = function (area, ...)
            return area.new (...)
        end
    }
)

Area.__index = Area
Area.__call = function (self)
    return Area.new (self)
end

function Area.new(fromPos, toPos, chunkSize, delay)
    local self = setmetatable ({ }, Area)
    self.fromPos = fromPos
    self.toPos = toPos
    self:adjustPos()
    self.chunkSize = tonumber(chunkSize) or defaultChunkSize
    self.delay = tonumber(delay) or defaultDelay
    return self
end

function Area:adjustPos()
    if self.toPos.x < self.fromPos.x then
        local tmp = self.toPos.x
        self.toPos.x = self.fromPos.x
        self.fromPos.x = tmp
    end
 
    if self.toPos.y < self.fromPos.y then
        local tmp = self.toPos.y
        self.toPos.y = self.fromPos.y
        self.fromPos.y = tmp
    end

    if self.toPos.z < self.fromPos.z then
        local tmp = self.toPos.z
        self.toPos.z = self.fromPos.z
        self.fromPos.z = tmp
    end
end

function Area:getFromPos()
    return self.fromPos
end

function Area:setFromPos(pos)
    self.fromPos = pos
    self:adjustPos()
end

function Area:getToPos()
    return self.toPos
end

function Area:setToPos(pos)
    self.toPos = pos
    self:adjustPos()
end

function Area:getChunkSize()
    return self.chunkSize
end

function Area:setChunkSize(chunkSize)
    self.chunkSize = tonumber(chunkSize) or defaultChunkSize
end

function Area:getDelay()
    return self.delay
end

function Area:setDelay(delay)
    self.delay = tonumber(delay) or defaultDelay
end

function Area:asyncIterateChunk(chunkPos, callback)
    local startPos = Position(self.fromPos.x + chunkPos.x * self.chunkSize, self.fromPos.y + chunkPos.y * self.chunkSize, self.fromPos.z + chunkPos.z)
 
    for offsetY = startPos.y, math.min(startPos.y + self.chunkSize - 1, self.toPos.y) do
    for offsetX = startPos.x, math.min(startPos.x + self.chunkSize - 1, self.toPos.x) do
        local currentPos = Position(offsetX, offsetY, startPos.z)
        callback(currentPos)
    end
    end
end

function Area:asyncIterateArea(callback)
    local chunkAmount = {
        x = math.floor((self.toPos.x - self.fromPos.x) / self.chunkSize),
        y = math.floor((self.toPos.x - self.fromPos.x) / self.chunkSize),
        z = (self.toPos.z - self.fromPos.z)
    }
 
    local n = 0
    for z = 0, chunkAmount.z do
    for y = 0, chunkAmount.y do
    for x = 0, chunkAmount.x do
        n = n + 1
        addEvent(Area.asyncIterateChunk, self.delay * n, self, {x = x, y = y, z = z}, callback)
    end
    end
    end
end

-- example code
--[[
function exampleFunction(pos, itemId)
    local tile = Tile(pos) or Game.createTile(pos)
    if tile then
        Game.createItem(itemId, 1, pos)
    end
end

function exampleCallback(pos)
    exampleFunction(pos, 1284)
end

function asyncTest()
    local area = Area(Position(160, 54, 7), Position(256, 256, 9), 32, 400)
    area:asyncIterateArea(exampleCallback)
end

addEvent(asyncTest, 4000)
]]

transformArea.lua:
Code:
BIOME_SPRING = 1
BIOME_SUMMER = 2
BIOME_AUTUMN = 3
BIOME_WINTER = 4

local biomeMachine = {
    { -- ground
        [BIOME_SPRING] = 106,
        [BIOME_SUMMER] = 4526,
        [BIOME_AUTUMN] = 8326,
        [BIOME_WINTER] = 670,
    },
    { -- border north west big
        [BIOME_SPRING] = 4792,
        [BIOME_SUMMER] = 4550,
        [BIOME_AUTUMN] = 8355,
        [BIOME_WINTER] = 4745,  
    },
    { -- tree
        [BIOME_SPRING] = 2712,
        [BIOME_SUMMER] = 2712,
        [BIOME_AUTUMN] = 2719,
        [BIOME_WINTER] = 2697,
    },
    { -- bush
        [BIOME_SPRING] = 2767,
        [BIOME_SUMMER] = 2767,
        [BIOME_AUTUMN] = 2784,
        [BIOME_WINTER] = 2684,
    },
    { -- fir tree
        [BIOME_SPRING] = 2700,
        [BIOME_SUMMER] = 2700,
        [BIOME_AUTUMN] = 2719,
        [BIOME_WINTER] = 2698,
    },
}

local biomeDatabase = {}

function registerBiomes()
    for i = 1, #biomeMachine do
        local itemsToRegister = {}
        for biomeId, itemId in pairs(biomeMachine[i]) do
            table.insert(itemsToRegister, itemId)
        end
  
        for j = 1, #itemsToRegister do
            local itemId = itemsToRegister[j]
            if not biomeDatabase[itemId] then
                biomeDatabase[itemId] = {}
            end
  
            for biomeId, toItemId in pairs(biomeMachine[i]) do
                biomeDatabase[itemId][biomeId] = toItemId
            end
        end
    end
end

registerBiomes()

function transferBiome(pos, toBiome)
    local tile = Tile(pos)
    if tile then
        local tileStack = tile:getItems()
        local ground = tile:getGround()
        if ground then
            table.insert(tileStack, ground)
        end
  
        for k, v in pairs(tileStack) do
            local itemId = v:getId()
            if biomeDatabase[itemId] and biomeDatabase[itemId][toBiome] then
                v:transform(biomeDatabase[itemId][toBiome])
            end  
        end
    end
end

-- example code
--[[
function biomeCallback(pos)
    transferBiome(pos, BIOME_SUMMER)
end

function biomeTransferTest()
    local fromPos = Position(201, 65, 7)
    local toPos = Position(221, 72, 7)
    local area = Area(fromPos, toPos, 4, 1000)
    area:asyncIterateArea(biomeCallback)
end

addEvent(biomeTransferTest, 10000)
]]
 
Hells yeah! This is hella nice! Thanks for the share! Can't wait to see these things for 1.5+
 
Good idea, I'm droping a comment to review it when I have time.

Not entirely sure how this is achieved in big servers or even global but I believe they make a reload in a different map instead of script-changing it. Did you benchmarked it?
 
Good idea, I'm droping a comment to review it when I have time.

Not entirely sure how this is achieved in big servers or even global but I believe they make a reload in a different map instead of script-changing it. Did you benchmarked it?

They load the pieces of map on server restart for world changes (every day at 9 am). For feyrist I believe that they update the map the same moment world light gets changed, though I don't have premium to check that out (don't want to buy one either because I don't have time to play). Alternatively they can use decay, but that reqires starting server at exact in-game hour.

Regarding the benchmarks: the larger the area, the more laggy the code will be obviously. If you won't set large areas/big chunks and small delays or execute some very complex function, your server shouldn't lag at all. For my map generator I've iterated over the dungeon area step by step (first cave tiles, then walkable grounds, then borders, etc)

It's the same tech I've used in my map generator and my map generator wasn't lagging the server at all with 100 online. This assuming you aren't trying to generate some 2000x2000x5 map - for this situation I'd recommend slicing the map to smaller areas (also async) before slicing them to chunks you'll be on so you don't bottleneck your server with adding sizeX/chunkSize * sizeY/chunkSize * sizeZ instructions to queue at once (78125 chunks of size 16x16 for 2000x2000x5 map) and don't take entire memory with timed events.
 
Last edited:
Updated version:
  • added support for sectors (map gets sliced to sectors so addEvent queue is smaller)
  • a sector of sectors scenario is possible - in this situation the code works as a fractal further reducing the lag and memory usage
  • added delay calculation so the chunks run at consistent speed and task queue has consistent size

video:
 

Attachments

Last edited:
nice one, can't wait to test it, will use it in my city for a different feeling

are you going to work in a version that change monsters too? 🤔 for example, each season have a different monster in the same spawn
 
nice one, can't wait to test it, will use it in my city for a different feeling

are you going to work in a version that change monsters too? 🤔 for example, each season have a different monster in the same spawn
I probably won't continue working on it. There are too many values to fill (I'd have to automate it somehow).
regarding monsters: you just have to hook Monster:onSpawn(position, startup, artificial) to get monster by position, read its name and replace it
 
a sector of sectors scenario is possible - in this situation the code works as a fractal further reducing the lag and memory usage
This is brilliant, absolutely love it
 
update:

bugfix: chunks near toPos aren't missing anymore
feature: biome template can now be loaded from the map
all examples can be found in the code itself

here is how to set up your own template:
1. place a rock 1304 at the corner (this is also your fromPos)
2. place signs with biomeId as a text
3. draw the different versions of the items
4. set toPos at bottom right corner of your structure

example template can be found in the map I posted as an attachment, at coordinates displayed on screenshot below:
1627641542801.png
Post automatically merged:

demo (click on video if autoplay doesn't work):
 

Attachments

This system really looks beautiful, although if you want to add more optimization you should save the tiles in a table and then iterate over them, instead of adding positions and creating the tiles in real time, this function Tile(pos) is one of the most expensive on the server and you use it too much in your whole system
 
This system really looks beautiful, although if you want to add more optimization you should save the tiles in a table and then iterate over them, instead of adding positions and creating the tiles in real time, this function Tile(pos) is one of the most expensive on the server and you use it too much in your whole system
for some reason TFS allows me to use 2 GB of my RAM only (despite being compiled in 64 bit mode) and large tables eat the RAM space pretty fast.
 
for some reason TFS allows me to use 2 GB of my RAM only (despite being compiled in 64 bit mode) and large tables eat the RAM space pretty fast.
perhaps its something in your build configuration? are you using release or debug?
 
Wouldn't it be easier to create multiple map files and load them in depending on the case?
 
Wouldn't it be easier to create multiple map files and load them in depending on the case?
I like this idea, I always pined to see it support by the map format + game engine. This was supported on pyOT, the map format was split into sec(tor) files and supported an instance dimension. The server was able to dynamically change the instance while running there might of been a limitation that noone could be on that sector of map while the change was happening.

Such a system would make it easier to have dynamic changing seasons and world landscape alternating events. In my mind I imagine map editors would be updated with the new instance dimension and you could toggle showing different instances and overlay them with an opacity making easier for mappers to align objects between the instances.
 
Back
Top