• 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!

[C++] Ground-Underground Mapping and Sunshine

Dries390

Well-Known Member
Joined
Sep 8, 2007
Messages
91
Solutions
4
Reaction score
70
Hello everyone. I recently ran into some problems tring to create more complex landscapes using the forgotten server engine and set out to try and bypass, if not fix, these following issues

1a)
The z =< 7 and z > 7 floors of the map don't communicate with each other. You can map something very nice below z = 7, but you will not be able to see it from above.
1b) Underground (z > 7) you can only see, at most, two floors down.
2) Remere's map editor has the same problem as 1a. When you map something below the ground, it's a pain to make sure everything lines up correctly on the surface.
3) The natural light cycle does not extend underground; even when it's light outside going under z = 7 will make it appear like you're in a cave.

A lot of smart people on the forums said a lot of smart things and so I was able to piece together how to perform all these changes. I've been sifting through a lot of random posts to try and piece everything together so I'll try to give credit to people where I remember who they were. Baxnie's post here:
showed me how to enable transparant water through otclient.



Requirements:

- You'll need to be able to re-compile Remere's map editor, your source of server (I use TFS 1.3 downgraded to 8.60 by Nekiro) and the OTClient. How to do this is explained at their respective github wiki's.

Disclaimer:

Please back up your files before messing with the source code. Things might go wrong and while I -can- usually fix my own code I can't guarantee I'll understand what's wrong with yours. Please use your head and try to learn -what- you're doing while applying these changes, my goal is really to write down as much as I can to help people figure out how to implement these changes rather than do everything for you. As far as I've tested, these changes work on my server and if anything goes really wrong I'll try to come back and update this.

Known Issues:

- I didn't put in the effort (yet) to fix how Remere offsets lower levels with respect to higher levels. This means your underground map will 'float' away when you go to z < 7, the lower levels keep being offset while the higher levels don't. This does -not- effect your map but it just looks really weird and might be a bit confusing.

Example:
post_1.PNG

This is how we want things to look when we're done (maybe mapped a lot better).

Server-side and OTClient-side
I based my modifications on Erexo's post here, all credits go to him:

TFS Modification
protocolgame.cpp l. 538 - l. 561
C++:
void ProtocolGame::GetMapDescription(int32_t x, int32_t y, int32_t z, int32_t width, int32_t height, NetworkMessage& msg)
{
    int32_t skip = -1;
    int32_t startz, endz, zstep;

    if (z > 7) {
        startz = 0;            //floors z < 7 retrieve map description from z = 0
        endz = 15;           //floors z < 7 look for map until z = 15
        zstep = 1;
    } else {
        startz = 15;          //floors z >= 7 look for map until z = 15
        endz = 0;             //floors z >= 7 retrieve map description from z = 0
        zstep = -1;
    }

    for (int32_t nz = startz; nz != endz + zstep; nz += zstep) {
        GetFloorDescription(msg, x, y, nz, width, height, z - nz, skip);
    }

    if (skip >= 0) {
        msg.addByte(skip);
        msg.addByte(0xFF);
    }
}

protocolgame.cpp l. 538 - l. 561
C++:
bool ProtocolGame::canSee(int32_t x, int32_t y, int32_t z) const
{
    if (!player) {
        return false;
    }

    const Position& myPos = player->getPosition();
    if (myPos.z <= 7) {
        //we are on ground level or above (7 -> 0)
        //view is from current floor to floor 15.
        if (z > 15) {
            return false;
        }
    } else if (myPos.z >= 8) {
        //we are underground (8 -> 15)
        //view is from current floor to floor 15.
        if (z > 15) {
            return false;
        }
    }

    //negative offset means that the action taken place is on a lower floor than ourself
    int32_t offsetz = myPos.getZ() - z;
    if ((x >= myPos.getX() - 8 + offsetz) && (x <= myPos.getX() + 9 + offsetz) &&
            (y >= myPos.getY() - 6 + offsetz) && (y <= myPos.getY() + 7 + offsetz)) {
        return true;
    }
    return false;
}

OTClient Modification

protocolgameparse.cpp l. 2044 - l. 2064
C++:
void ProtocolGame::setMapDescription(const InputMessagePtr& msg, int x, int y, int z, int width, int height)
{
    int startz, endz, zstep;

    if(z > Otc::SEA_FLOOR) {
        startz = 0;
        endz = 15;
        zstep = 1;
    }
    else {
        startz = 15;
        endz = 0;
        zstep = -1;
    }

    int skip = 0;
    for(int nz = startz; nz != endz + zstep; nz += zstep)
        skip = setFloorDescription(msg, x, y, nz, width, height, z - nz, skip);
}

map.cpp l. 707 - l. 710
C++:
int Map::getLastAwareFloor()
{
    return Otc::SEA_FLOOR + 8;
}

mapview.cpp l. 696 - l. 719
C++:
int MapView::calcLastVisibleFloor()
{
    if(!m_multifloor)
        return calcFirstVisibleFloor();

    int z = 7;

    Position cameraPosition = getCameraPosition();
    // this could happens if the player is not known yet
    if(cameraPosition.isValid()) {
        // view only underground floors when below sea level
        if(cameraPosition.z > Otc::SEA_FLOOR)
            z = 15;
        else
            z = 15;
    }

    if(m_lockedFirstVisibleFloor != -1)
        z = std::max<int>(m_lockedFirstVisibleFloor, z);

    // just ensure the that the floor is in the valid range
    z = stdext::clamp<int>(z, 0, (int)Otc::MAX_Z);
    return z;
}

These changes should allow your server, and client, to communicate floors above z = 7 with those below z = 7. Mapping this way is, however, a huge pain in the butt because Remere can't see below z = 7. The following piece of code "fixes" this:

map_drawer.cpp l. 100 - l. 134
C++:
void MapDrawer::SetupVars()
{
    canvas->MouseToMap(&mouse_map_x, &mouse_map_y);
    canvas->GetViewBox(&view_scroll_x, &view_scroll_y, &screensize_x, &screensize_y);

    dragging = canvas->dragging;
    dragging_draw = canvas->dragging_draw;

    zoom = (float)canvas->GetZoom();
    tile_size = int(TILE_SIZE / zoom); // after zoom
    floor = canvas->GetFloor();

    if(options.show_all_floors) {
        if(floor < 8)
            start_z = MAP_MAX_LAYER; // These two values for start_z allow the editor to get lower floors.
        else
            start_z = MAP_MAX_LAYER;  //start_z = std::min(MAP_MAX_LAYER, floor + 2);
    }
    else
        start_z = floor;

    end_z = floor;
    superend_z = (floor > GROUND_LAYER ? 8 : 0);

    start_x = view_scroll_x / TILE_SIZE;
    start_y = view_scroll_y / TILE_SIZE;

    if(floor > GROUND_LAYER) {
        start_x -= 2;
        start_y -= 2;
    }

    end_x = start_x + screensize_x / tile_size + 2;
    end_y = start_y + screensize_y / tile_size + 2;
}
Post automatically merged:

With the underground open, a new problem arises. Anyone going to z > 7 will be plunged into the depths of darkness because of how the day-night cycle is draw in the client, regardless of the fact that there is (maybe) no ground above it.

post_2.png
Here's a very crude illustration of the problem. I didn't like how "non-realistic" this was so I added the following code to my OTClient
mapview.cpp l. 117- l. 138
void MapView::draw(const Rect& rect)
C++:
                Light ambientLight;
                if(cameraPosition.z <= Otc::SEA_FLOOR) {
                    ambientLight = g_map.getLight();
                } else {
                    int floorCounter = 0;
                    for (int iz = cameraPosition.z; iz > 0; iz--) {
                        Position dPos= cameraPosition;
                        dPos.z = dPos.z - iz;
                        TilePtr dTile = g_map.getTile(dPos);
                        if (dTile) {
                            floorCounter = floorCounter + 1;
                        }
                    }
                    if (floorCounter> 0) {
                        ambientLight.color = 215;
                        ambientLight.intensity = 0;
                    } else {
                        ambientLight = g_map.getLight();
                    }
                }
                ambientLight.intensity = std::max<int>(m_minimumAmbientLight*255, ambientLight.intensity);
                m_lightView->setGlobalLight(ambientLight);
This little snipper of code essentially checks if there are any tiles above the current position and, if not, changes the light intensity to the "above-ground" light-intensity i.e. the current level of light corresponding to the time of day 'shines' down into the pit. Here's an example I just tested.

post_2.png

I'm still working on testing and improving the lighting. One thing I've (just) noticed is that there's a small bit of ambient light tied to the character underground such that the two brightnesses don't -quite- match up, but it's quite minor and I'll probably find a way to change it.

Edit: IMPORTANT! Don't forget to check "show all floors" or hit ctrl+W!

Enjoy!
 
Last edited:
For my purposes I kinda have to...

The og Frozenhell representation of the Inferno was insufficient for my tastes. I'd need 9 uninterrupted floors just to even start to do it right, but some levels would need 2-3 floors to really do justice, but honestly my vision is gonna take 14 floors. It should be pretty awesome. And don't even get me started on what comes after that.

Teleporter tricks are not a compromise. They are a cop out.

OP ran into a lot of problems because the current 0-15 floors are already so well defined in so many places. I think by leaving those alone, and creating a new range 16-47, one would preserve backwards compatibility and just have to worry about how the interface between 15 and 16 would work, since it's ocean floor to the top of a new sky. And then redoing light and visibility for the new range. It also makes a very clear distinction in that files utilizing this would choke the existing software making it clear they are different.
 
I ran into some more problems trying to create a tent in the pit I was working on. The light would drop to underground levels if I tried to enter the tent on the picture

1587140723380.png

because my script modification would detect the tiles and turn off the light. You can't just change the tiles to be non-ground because they won't pan the camera down when you walk under them. By changing line 10 of the last code above from

C++:
if (dTile) {

to

C++:
if (dTile && dTile->isFullGround()) {

You can now use Objectbuilder to make tiles which act like ground (camera pans down when you walk under them, you can examine them from a lower/higher level) but allow light to flow through.

1587140651605.png

Depending on what you're building it might be annoying to go through this process a lot (AND you can't walk on them as far as I know) but for smaller objects it's a really nice solution.

Edit: The observant reader might have noticed I forgot to unflag unpassability in the picture above. Uncheck that and update items.otb and you'll have normal-behaving floors you -can- walk on but which let sun flow down.
 
Last edited:
Update:

1) The second change of code is l. 639 - l. 667
2) IMPORTANT! In the OTClient sources you also need to modify the following function!

mapview.cpp, around l. 652 function calcFirstVisibleFloor
C++:
// limits to underground floors while under sea level
                if(cameraPosition.z > Otc::SEA_FLOOR)
                    firstFloor = 0; //std::max<int>(cameraPosition.z - Otc::AWARE_UNDEGROUND_FLOOR_RANGE, (int)Otc::UNDERGROUND_FLOOR)
I noticed too late that you were unable to see more than two floors up underground, this should fix it.

3) For people who are interested; I have devised a system that allows you to make zones that are permanently dark.
mapview.cpp l. 78
void MapView::draw(const Rect& rect)
C++:
                m_lightView->reset();
                m_lightView->resize(m_framebuffer->getSize());
                Light ambientLight;
                int darkCount = 0;
                for (int iz = 15; iz > cameraPosition.z; iz--) {
                    Position testPos = cameraPosition;
                    testPos.z = iz;
                    TilePtr tileDark = g_map.getTile(testPos);
                    if (tileDark) {
                        ItemPtr ground = tileDark->getGround();
                        if (ground) {
                            if (ground->getId() == 16122) {
                                darkCount = darkCount + 1;
                            }
                        }
                    }
                }
                  if (darkCount > 0) {
                    ambientLight.color = 215;
                    ambientLight.intensity = 0;
                } else if (cameraPosition.z <= Otc::SEA_FLOOR) {
                        ambientLight = g_map.getLight();
                } else {
                    int floorCounter = 0;
                    for (int iz = cameraPosition.z; iz > 0; iz--) {
                        Position dPos = cameraPosition;
                        dPos.z = dPos.z - iz;
                        TilePtr dTile = g_map.getTile(dPos);
                        if (dTile && dTile->isFullGround()) {
                            floorCounter = floorCounter + 1;
                        }
                    }
                    if (floorCounter > 0) {
                        ambientLight.color = 215;
                        ambientLight.intensity = 0;
                    } else {
                        ambientLight = g_map.getLight();
                    }
                }
                ambientLight.intensity = std::max<int>(m_minimumAmbientLight*255, ambientLight.intensity);
                m_lightView->setGlobalLight(ambientLight);

This code loops up from the bottom floor and checks if it finds a GROUNDtile (Important, it has to be ground) with CLIENTid of 16122. Obviously you can change the clientid to whatever cid you want. There's two things you have to watch out for:

1) It's a bit wonky so it might (and probably won't) work if the "dark" tiles are 7+ floors lower, so you might have to get creative at times.
2) Use a tile that doesn't give light. Mountain interiors apparantly allow light to come through and all your caves will turn into neon rave caves if you place tiles that give light under them.

1594553692832.png
 
Last edited:
Hello again everyone; Minor update for those who are either still following or stumble on this at some point in time. I fixed the shifting of the underground layers with respect to above ground because it was both annoying and confusing.

Simply change the following few lines:

map_drawer.cpp l. 1367- l. 1370
C++:
if (map_z <= GROUND_LAYER)
    offset = (GROUND_LAYER - map_z) * TILE_SIZE;
else
    offset = TILE_SIZE * (floor - map_z);

to

C++:
if (map_z <= GROUND_LAYER)
    offset = (GROUND_LAYER - map_z) * TILE_SIZE;
else
    if (floor <= GROUND_LAYER)
         offset = (GROUND_LAYER - map_z) * TILE_SIZE;
    else
        offset = TILE_SIZE * (floor - map_z);

I've added a few figures to illustrate exactly what this should fix.
 

Attachments

  • problem_floors1.PNG
    problem_floors1.PNG
    253 KB · Views: 48 · VirusTotal
  • problem_floors2.PNG
    problem_floors2.PNG
    233.2 KB · Views: 50 · VirusTotal
Another tiny update (I'm currently brushing up on C++ so it's nice to finally dot some i's). Mapping transparant tiles is currently rather annoying (I had to keep logging in and checking if everything lined up), so I added a (very rudimentary) bit of code to make anything selectively transparant.

map_drawer.cpp, l.27 directly under the includes
C++:
#include <fstream> // Import function to read text files

using namespace std;

ifstream inputFile{ "TransparantTiles.txt" };  // Read .txt
vector<uint32_t> transparant_tiles{ std::istream_iterator<uint32_t>{inputFile}, {} }; // Initialize vector of item IDs to turn transparant

It should look something like the picture.

newFunctionsTransparancy.PNG

map_drawer.cpp, void MapDrawer::BlitItem, l. 1048, change
C++:
// Ugly hacks. :)
    if(it.id == 0) {
        glDisable(GL_TEXTURE_2D);
        glBlitSquare(draw_x, draw_y, 255, 0, 0, alpha);
        glEnable(GL_TEXTURE_2D);
        return;
    } else if(it.id == 459 && !options.ingame) {
        glDisable(GL_TEXTURE_2D);
        glBlitSquare(draw_x, draw_y, red, green, 0, alpha/3*2);
        glEnable(GL_TEXTURE_2D);
        return;
    } else if(it.id == 460 && !options.ingame) {
        glDisable(GL_TEXTURE_2D);
        glBlitSquare(draw_x, draw_y, red, 0, 0, alpha/3*2);
        glEnable(GL_TEXTURE_2D);
        return;
    }

to

C++:
// Ugly hacks. :)
    if(it.id == 0) {
        glDisable(GL_TEXTURE_2D);
        glBlitSquare(draw_x, draw_y, 255, 0, 0, alpha);
        glEnable(GL_TEXTURE_2D);
        return;
    } else if(it.id == 459 && !options.ingame) {
        glDisable(GL_TEXTURE_2D);
        glBlitSquare(draw_x, draw_y, red, green, 0, alpha/3*2);
        glEnable(GL_TEXTURE_2D);
        return;
    } else if(it.id == 460 && !options.ingame) {
        glDisable(GL_TEXTURE_2D);
        glBlitSquare(draw_x, draw_y, red, 0, 0, alpha/3*2);
        glEnable(GL_TEXTURE_2D);
        return;
    } else if (find(transparant_tiles.begin(), transparant_tiles.end(), item->getClientID()) != transparant_tiles.end()) {
        alpha /= 2; // Checks the id of the drawn item against the vector of transparant items and reduces the alpha to make it transparant
    }

All you need to do next is create a .txt file in the same directory as RME_x64.exe called TransparantTiles.txt and add the clientID of the tiles you want to turn transparant (1 per line!). These are the same IDs you entered in the things.otml file so you should be able to copy-paste them. I turned a rock transparant in the second picture and fixed the transparant water, in RME, in the third picture.

spawnsalot2.PNGspawnsalot3.PNG
 
Final update for a while, not directly sure if anything needs fixing at the moment. My central question was the following; if you, the player, can see through water, why can't your character? This should fix this.

data\event\scripts\player.lua
Lua:
function Player:onLook(thing, position, distance)
    local description = "You see " .. thing:getDescription(distance)
    local transparantTiles = {470, 15850, 15919, 15920, 15921, 15922, 15923, 15924, 16120} -- Array of ClientIds for things you want to look through
    local t = thing:getType()

    if t and not thing:isMonster () then
        local c = t:getClientId() 
        if c then
            if isInArray(transparantTiles, c) then -- Checks if tiles are look-throughable
                local posThing = thing:getPosition()
                local tile = nil
                repeat
                    posThing.x = posThing.x - 1
                    posThing.y = posThing.y - 1
                    posThing.z = posThing.z + 1
                    tile = Tile(posThing.x, posThing.y, posThing.z)
                until tile
                
                
                if tile then
                    local top = tile:getTopDownItem()
                    if not top then
                        top = tile:getTopTopItem()
                    end
                    local creatureTile = tile:getTopCreature()
                    local ground = tile:getGround()
                    if creatureTile then
                        description = "You see " .. creatureTile:getDescription(distance)
                    elseif top then
                        description = "You see " .. top:getDescription(distance)
                    else
                        description = "You see " .. ground:getDescription(distance)
                    end
                end
            end
        end
    end
    
   ...

This apparantly only works for older versions of TFS (< 1.5) so if I ever decide to upgrade to a newer version of I'll come back and update the code. It's been a while since I've lua'd so the code, using a while loop, might not really play nice so be careful with not putting tiles under transparant tiles!

Please report any bugs you find in this thread, I'll try and work through them as fast as possible.
 

Attachments

Hey bro, amazing tutorial :D
Just to let you know it works both on mehah and on otcv8 (I think you made the tutorial based on edubart).
The only part that is not matching is the light fix that you were implementing (light underground if no ground above the head).
I think I managed to solve that for otcv8, now the thing left would be making it work on Mehah. The problem was that they don't exactly have the void MapView::draw(const Rect& rect). I might post the changes for otcv8 on github some time later and hopefully someone can have an idea for Mehah's.
 
Yes, I did use Edubart; should have probably mentioned that. I don't spend a lot of time on the forums (but I have gotten more active working on my files recently!), if anyone needs help I can always try my best.

Glad to see you like it/it's useful!
 
Okay, so the otcv8 part is:

instead of doing what was told here:
Lua:
Here's a very crude illustration of the problem. I didn't like how "non-realistic" this was so I added the following code to my OTClient
mapview.cpp l. 117- l. 138
void MapView::draw(const Rect& rect)
find in
void MapView::drawMapBackground(const Rect& rect, const TilePtr& crosshairTile) {

the lines
if (m_drawLight) {

and the whole if change for:

Code:
if (m_drawLight) {
       Light ambientLight;
                if(cameraPosition.z <= Otc::SEA_FLOOR) {
                    ambientLight = g_map.getLight();
                } else {
                    int floorCounter = 0;
                    for (int iz = cameraPosition.z; iz > 0; iz--) {
                        Position dPos= cameraPosition;
                        dPos.z = dPos.z - iz;
                        TilePtr dTile = g_map.getTile(dPos);
                        if (dTile && dTile->isFullGround()) {
                            floorCounter = floorCounter + 1;
                        }
                    }
                    if (floorCounter > 0) {
                        ambientLight.color = 215;
                        ambientLight.intensity = 0;
                    } else {
                        ambientLight = g_map.getLight();
                    }
                }
                ambientLight.intensity = std::max<int>(m_minimumAmbientLight*255, ambientLight.intensity);
               
                if (!m_lightTexture || m_lightTexture->getSize() != m_drawDimension)
                    m_lightTexture = TexturePtr(new Texture(m_drawDimension, false, true));
                m_lightView = std::make_unique<LightView>(m_lightTexture, m_drawDimension, rect, srcRect, ambientLight.color, std::max<int>(m_minimumAmbientLight * 255, ambientLight.intensity));
    }
Indentation or whatever this word is written like can be fixed. :p
 
Last edited:
Hello again everyone; another quick update:

Whilst working on some pits in the z-level 7 level I found out that I couldn't throw items up/down "through" the 7th level. There's an exceedingly easy fix for this:
in map.cpp, function: bool Map::canThrowObjectTo(...) simply comment out

C++:
if ((fromPos.z >= 8 && toPos.z < 8) || (toPos.z >= 8 && fromPos.z < 8)) {
    return false;
}

And it should function just like any other level-transition.
 
I rewrote the last code snippet to be slightly less "noobish", I'm not sure how much of a difference it makes but this should be more efficient.

mapview.cpp l. 117- l. 138
void MapView::draw(const Rect& rect)
C++:
if (cameraPosition.z <= Otc::SEA_FLOOR) {
    ambientLight = g_map.getLight();
} else {
    Position dPos = cameraPosition;
    --dPos.z;
    TilePtr dTile;
    for (dPos.z; dPos.z > 0; --dPos.z) {
        dTile = g_map.getTile(dPos);
        if (dTile && dTile->isFullGround()) {
            ambientLight.color = 215;
            ambientLight.intensity = 0;
            break;
        }
        ambientLight = g_map.getLight();
    }
}
ambientLight.intensity = std::max<int>(m_minimumAmbientLight*255, ambientLight.intensity);
m_lightView->setGlobalLight(ambientLight);

It breaks out of the loop as soon as the condition is met and avoids recreating the same variable on every iteration.
 
Hello @Dries390


I have this same problem:


Could you help us please?
 
Last edited:

Similar threads

Back
Top