Source Development Dynamic Portals

ximboliex

Pirate
Registered
LV
0
 
Joined
Jan 29, 2026
Messages
16
Reaction score
12
Points
8
Location
Panama
This idea was inspired by "Angelix"

This was the spoiler I made in a previous post; it was about dynamic portals. I made them both personal and global, for use by NPCs for functions, missions, etc.
This is just a starting point that you can improve and optimize. I'm leaving you with the canvas to work on if you find this function useful.

Okay, let's begin.

Src/GameServer/SubMap.h in top paste it:
C++:
#include <map>


struct SDynamicPortal
{
    long lPortalID;

    long lOwnerChaID;

    DWORD dwCloseTick;

    std::string strFunction;

    std::string strName;

    CItem* pItem;

    SubMap* pMap;

    BYTE byPortalType;
};

SubMap.h: search class SubMap and inside public: add this
C++:
    long CreateDynamicPortal(long lPosX,long lPosY,const char* szFunction,const char* szName,long lLifeTime,long lItemID = 193);
    long CreateDynamicPortalCha(CCharacter* pOwner,long lPosX,long lPosY,const char* szFunction,const char* szName,long lLifeTime,long lItemID = 193);
    bool DestroyDynamicPortal(long lPortalID);

Now in same file SubMap.h just before the endif add this:
C++:
extern std::map<long, SDynamicPortal> g_DynamicPortalList;
extern long g_lDynamicPortalID;


Now add this SubMap.cpp top:
C++:
std::map<long, SDynamicPortal> g_DynamicPortalList;
long g_lDynamicPortalID = 1;


Now at the end of the file GameServer/SubMap.cpp add this:
C++:
long SubMap::CreateDynamicPortal(
    long lPosX,
    long lPosY,
    const char* szFunction,
    const char* szName,
    long lLifeTime,
    long lItemID
)
{
    SItemGrid SItemCont;

    SItemCont.sID = (short)lItemID;
    SItemCont.sNum = 1;
    SItemCont.SetDBParam(-1, 0);
    SItemCont.chForgeLv = 0;
    SItemCont.SetInstAttrInvalid();

    CEvent CEvtCont;

    CEvtCont.SetID(1);
    CEvtCont.SetTouchType(enumEVENTT_RANGE);
    CEvtCont.SetExecType(enumEVENTE_DYNAMIC_PORTAL);

    CItem* pItem =
        ItemSpawn(
            &SItemCont,
            lPosX,
            lPosY,
            enumITEM_APPE_NATURAL,
            0,
            g_pCSystemCha->GetID(),
            g_pCSystemCha->GetHandle(),
            -1,
            -1,
            &CEvtCont
        );

    if (!pItem)
        return 0;

    pItem->SetOnTick(0);

    long lPortalID =
        g_lDynamicPortalID++;

    SDynamicPortal PortalData;

    PortalData.lPortalID =
        lPortalID;

    PortalData.lOwnerChaID =
        0;

    PortalData.strFunction =
        szFunction;

    PortalData.strName =
        szName;

    if (lLifeTime < 0)
    {
        PortalData.dwCloseTick =
            0;
    }
    else
    {
        PortalData.dwCloseTick =
            GetTickCount() +
            (lLifeTime * 1000);
    }

    PortalData.pItem =
        pItem;

    PortalData.pMap =
        this;

    g_DynamicPortalList[lPortalID] =
        PortalData;

    LG(
        "portal",
        "ADD GLOBAL PORTAL ID: %d\n",
        lPortalID
    );

    return lPortalID;
}

long SubMap::CreateDynamicPortalCha(
    CCharacter* pOwner,
    long lPosX,
    long lPosY,
    const char* szFunction,
    const char* szName,
    long lLifeTime,
    long lItemID
)
{
    if (!pOwner)
        return 0;

    SItemGrid SItemCont;

    SItemCont.sID = (short)lItemID;
    SItemCont.sNum = 1;
    SItemCont.SetDBParam(-1, 0);
    SItemCont.chForgeLv = 0;
    SItemCont.SetInstAttrInvalid();

    CEvent CEvtCont;

    CEvtCont.SetID(1);
    CEvtCont.SetTouchType(enumEVENTT_RANGE);
    CEvtCont.SetExecType(enumEVENTE_DYNAMIC_PORTAL);

    CItem* pItem =
        ItemSpawn(
            &SItemCont,
            lPosX,
            lPosY,
            enumITEM_APPE_NATURAL,
            0,
            g_pCSystemCha->GetID(),
            g_pCSystemCha->GetHandle(),
            -1,
            -1,
            &CEvtCont
        );

    if (!pItem)
        return 0;

    pItem->SetOnTick(0);

    long lPortalID =
        g_lDynamicPortalID++;

    SDynamicPortal PortalData;

    PortalData.lPortalID =
        lPortalID;

    PortalData.lOwnerChaID =
        pOwner->GetID();

    PortalData.strFunction =
        szFunction;

    PortalData.strName =
        szName;

    if (lLifeTime < 0)
    {
        PortalData.dwCloseTick =
            0;
    }
    else
    {
        PortalData.dwCloseTick =
            GetTickCount() +
            (lLifeTime * 1000);
    }

    PortalData.pItem =
        pItem;

    PortalData.pMap =
        this;

    g_DynamicPortalList[lPortalID] =
        PortalData;

    LG(
        "portal",
        "ADD PLAYER PORTAL ID: %d OWNER: %d\n",
        lPortalID,
        pOwner->GetID()
    );

    return lPortalID;
}


bool SubMap::DestroyDynamicPortal(
    long lPortalID
)
{
    std::map<long, SDynamicPortal>::iterator it =
        g_DynamicPortalList.find(
            lPortalID
        );

    if (
        it ==
        g_DynamicPortalList.end()
    )
    {
        return false;
    }

    CItem* pItem =
        it->second.pItem;

    if (!pItem)
    {
        g_DynamicPortalList.erase(
            it
        );

        return false;
    }

    GoOut(
        pItem
    );

    pItem->Free();

    g_DynamicPortalList.erase(
        it
    );

    LG(
        "portal",
        "REMOVE PORTAL ID: %d\n",
        lPortalID
    );

    return true;
}
Now search void SubMap::Run(DWORD dwCurTime) inside SubMap.cpp and right after this
C++:
    if (m_timeSpecialRun.IsOK(dwCurTime))
    {
        string    strScript = "map_copy_run_special_";
        strScript += GetName();
        g_CParser.DoString(strScript.c_str(), enumSCRIPT_RETURN_NONE, 0, enumSCRIPT_PARAM_LIGHTUSERDATA, 1, this, DOSTRING_PARAM_END);

    }

Add This:
C++:
    // Dynamic Portal Auto Close
    for(std::map<long, SDynamicPortal>::iterator it = g_DynamicPortalList.begin(); it != g_DynamicPortalList.end(); ++it)
    {
        if(it->second.dwCloseTick > 0 && GetTickCount() >=it->second.dwCloseTick)
        {
            DestroyDynamicPortal(it->first);
        }
    }

Now in EntityScript.cpp add this functions:
C++:
//=========================================================
// Dynamic Portals - [email protected]
//=========================================================
inline int lua_CreateDynamicPortal(lua_State* L)
{
    BOOL bValid =
        lua_gettop(L) >= 6 &&
        lua_isnumber(L, 2) &&
        lua_isnumber(L, 3) &&
        lua_isstring(L, 4) &&
        lua_isstring(L, 5) &&
        lua_isnumber(L, 6);

    if (!bValid)
    {
        E_LUAPARAM;
        return 0;
    }

    SubMap* pMap = (SubMap*)lua_touserdata(L,1);

    if (!pMap)
    {
        E_LUANULL;
        return 0;
    }

    long lPosX = (long)lua_tonumber(L, 2);

    long lPosY = (long)lua_tonumber(L, 3);

    const char* szFunction = lua_tostring(L, 4);

    const char* szName = lua_tostring(L, 5);

    long lLifeTime = (long)lua_tonumber(L, 6);

    long lItemID = 193;

    if (lua_gettop(L) >= 7)
    {
        lItemID =(long)lua_tonumber(L,7);
    }

    long lPortalID =
        pMap->CreateDynamicPortal(
            lPosX,
            lPosY,
            szFunction,
            szName,
            lLifeTime,
            lItemID
        );

    lua_pushnumber(L,lPortalID);

    return 1;
}

inline int lua_CreateDynamicPortalCha(lua_State* L)
{
    BOOL bValid =
        lua_gettop(L) >= 7 &&
        lua_isnumber(L, 3) &&
        lua_isnumber(L, 4) &&
        lua_isstring(L, 5) &&
        lua_isstring(L, 6) &&
        lua_isnumber(L, 7);

    if (!bValid)
    {
        E_LUAPARAM;
        return 0;
    }

    CCharacter* pOwner = (CCharacter*)lua_touserdata(L,1);

    SubMap* pMap = (SubMap*)lua_touserdata(L,2);

    if (!pOwner || !pMap)
    {
        E_LUANULL;
        return 0;
    }

    long lPosX = (long)lua_tonumber(L, 3);

    long lPosY = (long)lua_tonumber(L, 4);

    const char* szFunction = lua_tostring(L, 5);

    const char* szName = lua_tostring(L, 6);

    long lLifeTime = (long)lua_tonumber(L, 7);

    long lItemID = 193;

    if (lua_gettop(L) >= 8)
    {
        lItemID = (long)lua_tonumber(L,8);
    }

    long lPortalID =
        pMap->CreateDynamicPortalCha(
            pOwner,
            lPosX,
            lPosY,
            szFunction,
            szName,
            lLifeTime,
            lItemID
        );

    lua_pushnumber(
        L,
        lPortalID
    );

    return 1;
}

inline int lua_DestroyDynamicPortal(lua_State* L)
{
    BOOL bValid =lua_gettop(L) == 1 && lua_isnumber(L, 1);

    if (!bValid)
    {
        E_LUAPARAM;
        return 0;
    }

    long lPortalID = (long)lua_tonumber(L,1);

    bool bResult = false;

    std::map<long, SDynamicPortal>::iterator it = g_DynamicPortalList.find(lPortalID);

    if ( it != g_DynamicPortalList.end() )
    {
        bResult = it->second.pMap->DestroyDynamicPortal(lPortalID);
    }

    lua_pushnumber(L,bResult ? LUA_TRUE : LUA_FALSE);

    return 1;
}

inline int lua_DestroyDynamicPortalCha(lua_State* L)
{
    BOOL bValid = lua_gettop(L) == 1;

    if (!bValid)
    {
        E_LUAPARAM;
        return 0;
    }

    CCharacter* pCha = (CCharacter*)lua_touserdata(L,1);

    if (!pCha)
    {
        E_LUANULL;
        return 0;
    }

    long lDestroyedCount = 0;

    for (std::map<long, SDynamicPortal>::iterator it = g_DynamicPortalList.begin();
        it != g_DynamicPortalList.end();
    )
    {
        if (it->second.lOwnerChaID ==pCha->GetID())
        {
            long lPortalID = it->first;

            SubMap* pMap = it->second.pMap;

            ++it;

            if (pMap->DestroyDynamicPortal(lPortalID))
            {
                lDestroyedCount++;
            }
        }
        else
        {
            ++it;
        }
    }

    lua_pushnumber(L,lDestroyedCount);

    return 1;
}

Now we'll register them in this same file (to keep track, I registered them right here).
search BOOL RegisterEntityScript() and add this:
Code:
    REGFN(CreateDynamicPortal);
    REGFN(CreateDynamicPortalCha);
    REGFN(DestroyDynamicPortal);
    REGFN(DestroyDynamicPortalCha);

Now in Characterprl.cpp search case enumACTION_EVENT and reemplace all case for this:
Code:
    case enumACTION_EVENT:
    {
        Long lID = READ_LONG(pk);
        Long lHandle = READ_LONG(pk);

        Entity* pCObj = g_pGameApp->IsLiveingEntity(lID,lHandle);

        if (!pCObj)
        {
            m_CLog.Log("it inexistent this entity in this map");
            break;
        }

        uShort usEventID = READ_SHORT(pk);

        CEvent& event = pCObj->GetEvent();

        //--------------------------------------------------
        // DYNAMIC PORTAL
        //--------------------------------------------------
        if (event.GetExecType() == enumEVENTE_DYNAMIC_PORTAL)
        {
            CItem* pItem =(CItem*)pCObj;

            bool bFound = false;

            for (std::map<long, SDynamicPortal>::iterator it = g_DynamicPortalList.begin();
                it != g_DynamicPortalList.end();
                ++it
            )
            {
                if (it->second.pItem == pItem )
                {
                    bFound = true;

                    lua_getglobal(g_pLuaState,it->second.strFunction.c_str());

                    if (!lua_isfunction(g_pLuaState,-1))
                    {
                        LG("dynamic_portal","Lua function not found: %s\n",it->second.strFunction.c_str());
                        lua_pop(g_pLuaState,1);
                        break;
                    }

                    lua_pushlightuserdata(g_pLuaState,this);

                    lua_pushlightuserdata(g_pLuaState,GetSubMap());

                    if (lua_pcall(g_pLuaState,2,0,0) != 0)
                    {
                        const char* szError = lua_tostring(g_pLuaState,-1);

                        LG("dynamic_portal","Lua Error: %s\n",szError);

                        lua_pop(g_pLuaState,1);
                    }

                    break;
                }
            }

            if (!bFound)
            {
                LG("dynamic_portal","Portal entity not found in portal list\n");
            }

            break;
        }

        ExecuteEvent(pCObj,usEventID);
    }
    break;

Now go to Src/Libraries/common/include/EventRecord.h search enum EEventExecType and add this:
C++:
enumEVENTE_DYNAMIC_PORTAL = 3,

and Done, Now I will give basic examples of how to use the functions(I used usable items here as a test; it was more practical to do the tests that way, but you can apply the same logic to anything you want.)

Dynamic Portal System - Basic Functions


The Dynamic Portal system allows Lua scripts to create temporary or permanent interactive portals during gameplay. These portals can execute custom Lua functions when a player interacts with them.
Code:
function TestPortal(Player, MapCopy)

    SystemNotice(Player,"Dynamic Portal Works!")

end
This is the callback function assigned to a Dynamic Portal. When a player interacts with the portal, this function is automatically executed by the server.

Parameters:
Player → The character who activated the portal.
MapCopy → The current map instance where the portal exists.


Dynamic Portal
Code:
function DynamicPortal(Player, Item)

    local MapCopy = GetChaMapCopy(Player)

    local x, y = GetChaPos(Player)

    local PortalID = CreateDynamicPortalCha(
            Player,
            MapCopy,
            x + 200,
            y,
            "TestPortal",
            "Player Portal",
            10
        )

    SystemNotice(Player,"Player Portal ID: "..PortalID)

end

Creates a player-owned Dynamic Portal near the character's current position.

How it works:
  1. Retrieves the player's current map.
  2. Retrieves the player's current coordinates.
  3. Creates a Dynamic Portal 200 units in front of the player.
  4. Assigns the callback function TestPortal.
  5. Sets the portal display name to "Player Portal".
  6. Sets a lifetime of 10 seconds if you set -1 time portal never close alone
  7. Returns the generated Portal ID.

Destroys all Dynamic Portals owned by the player.
Code:
function DestryPortal(Player, Item)

    local Count = DestroyDynamicPortalCha(Player)

    SystemNotice(Player,"Destroyed "..Count.." portal(s)")

end
How it works:
  1. Searches for every portal created by the specified player.
  2. Removes all matching portals from the map.
  3. Returns the number of portals removed.


This system allows Community to create custom teleports, dungeon entrances, event triggers, quest portals, temporary gateways, and many other gameplay mechanics entirely through Lua.

Enjoy
 

Attachments

  • Like
Reactions: Syborg and zLuke
this idea been made 2y+ ago first idea came from @Perseus , and back in days there is no ai,agent's to review so was mostly broken unfinished
i had to fix it reusing CSwitchMapRecord instead of new struct's


 
This way it can also be used; it's an incredible use. What I was saying is that this base I published can be modified to do whatever you want. Handlechat would be one way to give it utility through the many applications.
Can make portal like party portal, guilds portals, etc

PS: I didn't know Perseus had already thought of something similar. Great;
 
Last edited: