Metin2 Hungarian Forum

Metin2 => Privát szerverek => Szerver készítés => A témát indította: masodikbela Dátum 2016-04-09, 20:54:05

Cím: [C++] A '/reload q' ezeréves crashe
Írta: masodikbela Dátum 2016-04-09, 20:54:05
Nos igen rég volt már, hogy alkalmam nyílt valamit is készíteni, de mivel ma összejöttek a dolgok, ezért most írhatok ismét egy esti mesét.

[spoiler=Fix]Na persze, mielőtt bármibe is belevágnék vagy mondanék bármit is, kezdjük a fixxel (a többséget úgy is csak ez érdekli, az nem fontos milyen fix :P)

src/game

questmanager.h:
Ez alá:PC * GetPCForce(unsigned int pc);
Illeszd be ezt:void StopAllRunningQuests();

questmanager.cpp:
Ez alá: void CQuestManager::DisconnectPC(LPCHARACTER ch)
{
m_mapPC.erase(ch->GetPlayerID());
}

ezt: void CQuestManager::StopAllRunningQuests()
{
for (PCMap::iterator it = m_mapPC.begin(); it != m_mapPC.end(); it++)
{
it->second.CancelRunning();
LPCHARACTER pkChr = CHARACTER_MANAGER::instance().FindByPID(it->first);
if (!pkChr || !(pkChr->GetDesc()))
continue;
struct ::packet_script packet_script;

packet_script.header = HEADER_GC_SCRIPT;
packet_script.skin = QUEST_SKIN_NOWINDOW;
string data = "[DESTROY_ALL]";
packet_script.src_size = data.size();
packet_script.size = packet_script.src_size + sizeof(struct packet_script);

TEMP_BUFFER buf;
buf.write(&packet_script, sizeof(struct packet_script));
buf.write(&data[0], data.size());

pkChr->GetDesc()->Packet(buf.read_peek(), buf.size());
}
}


Ugyan ebben a fájlban ez alá: void CQuestManager::Reload()
{

Ezt:StopAllRunningQuests();

Okay, a game kész is.

Binary

PythonNetworkStream.h:
Ez alá:void OnScriptEventStart(int iSkin, int iIndex);
Illeszd be ezt:void HideQuestWindows();

PythonNetworkStream.cpp:
A fájl legaljára illeszd be ezt:void CPythonNetworkStream::HideQuestWindows()
{
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "HideAllQuestWindow", Py_BuildValue("()"));
}


PythonNetworkStreamPhaseGame.cpp:
A bool CPythonNetworkStream::RecvScriptPacket() funkción belül ez alá:str[str.size()-1] = '\0';
Illeszd be ezt: if (str.compare(0, 13, "[DESTROY_ALL]") == 0)
{
CPythonNetworkStream::Instance().HideQuestWindows();
return true;
}


Okay, az indítónk is készen van.

Root fájlok

interfacemodule.py:
E fölé:def RemoveQuestDialog(self, key):
Illeszd be ezt: def HideAllQuestWindow(self):
tempList = []
for i,v in self.wndQuestWindow.iteritems():
tempList.append(v)

for i in tempList:
i.OnCancel()


game.py:
Ez alá:def OpenQuestWindow(self, skin, idx):
self.interface.OpenQuestWindow(skin, idx)

Illeszd be ezt: def HideAllQuestWindow(self):
self.interface.HideAllQuestWindow()


Ééés készen is vagyunk.
[/spoiler]

[spoiler=Magyarázat (aka esti mese rész)]
Szóóval mire is jó ez?

Ugyebár ismerjük mindannyian az íratlan szabályt: éles szerveren nem "/reload q"-zunk! Na persze, az ilyen kijelentéseket/tévhiteket én mindig tagadni szoktam, mint például: "Otthon nem csinálhatsz root szervert, csak szervergépen! Ha otthon csinálsz szervert kell az MC!" - no comment; "Object mappát nem piszkáljuk!" - jah persze, de ha a syserr kiad egy sort hogy hol hibás a quest, nem a questben kéne nézni azt a sort, hanem az object mappában...

Na de a legelső "igazságra" visszatérve, miszerint éles szerveren nem "/reload q"-zunk, igaz! volt... eddig... Ezen probléma alakalmából úgy gondoltam, jó ötlet volna egy kis "magyarázatot" is beletenni a dologba, hátha valaki tanulni akar a dologból, és nem csak berakni a fixet és jólvanazúgy... Tehát a továbbiakban egy "átlagos" hibajavítási/keresési folyamatról fogok beszélni, vagyis én hogyan szoktam az ilyen dolgokat orvosolni/megoldani.

Természetesen egy videó is készült: katt (https://www.youtube.com/watch?v=QpsDRS1qcHY&feature=youtu.be)

Kezdjük az elején: A probléma

Pár napja megkeresett egy barátom, hogy nem e lenne kedvem fixet írni arra, hogy ne crasheljen be a game amikor használja a kérdéses parancsot az éles szerveren. Persze az volt az első, hogy mondtam: éles szerveren nem kellene reloadozni... De persze ezt az illető nem hagyta annyiban, mondta, hogy ezt ő is tudja, de elég jó lenne ha a questek miatt nem kéne teljes resit csinálni.

Körülbelül ennyi is volt a "probléma ismertetése" rész. Tehát értékes információ ami eddig a birtokunkban van:

  • /reload q parancs alapjáraton jól működik, teszi a dolgát, ha csak alig vannak fent (pl tesztszeró)
  • a kérdéses parancs ugyan akkor nem működik rendesen, ha sok játékos van fent, az esetek túlnyomó többségében becrashel az adott core

  • 1. lépés: A probléma előidézése

    Legfontosabb dolog mindenek előtt, mielőtt bármi kódot is néznénk, hogy elő tudjuk e tesztkörnyezetben idézni a problémát. Jelen esetben elég kevés információ áll rendelkezésünkre, de ennek ellenére tapasztaltabbaknak már van némi tippje, hogy hol is lesz a kutya elásva, így nekem is volt. Írtam egy nagyon rövid questet, amiben volt 2 say, és köztük egy wait(). (Ez a videóban nem látszik, mivel megírtam mielőtt elkezdtem felvenni.)

    Mivel láttam már korábban a game forrásában a questekért felelős részt, ezért emlékeztem rá, hogy a questek egy megszakításos módszeren alapulnak. Tehát amint jön a quest kódjában egy input() parancs, megáll az egész, és kirak automatikusan egy OK gombot (gondolom ezt már mindenki tapasztalta). Ilyenkor az adott quest "suspendelésre" kerül, azaz annál a sornál megáll a végrehajtás, amelyikben az input szerepel, és addig nem is halad tovább, míg a kliensben a játékos rá nem nyom az elfogadásra. (Egyébként waitnál/selectnél is ugyan ez a mechanizmus.)

    Ugyanakkor azt is tudni kell, mit csinál a reload q. Nos korábban már őhozzá is volt szerencsém, így tudtam, hogy az összes timert leállítja, törli, illetve minden eddig betöltött questet teljesen kitöröl a memóriából, és újra betölti azokat.

    És itt össze is áll a kép: reload q esetén kitörlődnek a memóriából az egyes "quest állapotok", hogy éppen hol tart egy adott kód, hol van suspendelve ha egy player éppen egy questet olvas/csinál. Azonban lehetséges, hogy ezek az állapotok a játékosoknál csak pointerként vannak eltárolva, tehát csak egy adott memóriaterületre mutatnak ezek, ahol valójában fizikailag tárolva vannak.

    Tehát tegyük fel: egy játékos éppen olvas egy questet, reload q-zol. Utána ő rányom a tovább gombra. A kliens elküldi a szervernek a gomb számát, az pedig annak megfelelően "resume"-olni akarja az adott questet, hogy az fusson tovább. Igen ám, a pointer még meg is van, ahhoz nem nyúltunk, viszont ahova mutat, ott már csak "a hatalmas űr van, és egy fekete lyuk tátong ott, ami szépen be is szippantja az egész programunkat", ami azonnali hatállyal fel is mond, és becrashel... [A videó elején látható is egy példa (az egész egyébként amit felvettem első ránézésre volt, tehát nem csináltam meg sunyiban előtte, hogy tudjam miről van szó...)]

    2. lépés: Hogyan kellene megoldani?

    Sikeresen leteszteltük a feltevést, valóban becrashel a programunk, tehát jó úton haladunk a Teljesség felé. Gyakorlatilag most még eddig kódba nem biztos, hogy belenéztünk, de minden esetre nagyjából tudjuk hogy kb hol/mi lehet a baj.

    A jelenlegi problémánk mivel crashelés, és nem csak valami "rendszer" hiányos/nem megfelelő működése, ezért debuggerek a rendelkezéseinkre állnak. A "tehetősebbek" (nem anyagilag!) windowson tesztelnek, így van visual studiójuk, a többieknek pedig a csodálatos gdb és a freebsd marad. Akárhogy is, kapunk egy brakepointot, és el tudunk indulni valamerre. Jelen esetben egy lua részen belül, valami "resume" funkciónál találjuk a problémát, azt sem tudjuk mi az, de persze nem kell megijedni, és megérteni sem kell az ott lévő kódot. Ha kicsit tovább megyünk, látjuk azt a funkciót, ami az előző lefutása után következik, tehát folyamatosan kifelé haladunk a kódból.

    Jelen esetben a 2./3. már elég is volt, hogy megtaláljuk a változónkat, amit törölni kellene, mivel látjuk a használata előtt van egy ellenőrzés, miszerint ha nincs semmire állítva (NULL-on van) akkor nem fog semmi sem történni, csak a syserr kiír majd egy hibát, és kész. (m_RunningQuestState változóra gondolok)

    Zsír, most végre tudjuk a KONKRÉT okot, amit ki kell küszöbölnünk. Tehát gondoljunk csak bele. Minden játékosnál ezt végig kell csinálnunk: lenullázni ezt a változót. Tehát szükségünk lesz egy for-ra, és ehhez kellene még keresünk valamit, ami tárolja az összes játékos esetén azt a változót, vagy azt az osztályt (jelen esetben az utóbbi) ami tartalmazza azt a változót. Ha szerencsénk van a kódokat átbújva (általában az ilyen dolgok mindig a "managerekben" vannak, mivel azok singletonok, így csak egyetlen egyszer jönnek létre, tehát nem kell sehol sem eltárolnunk a címét, bárhonnan el tudjuk érni azt az egyet, így alkalmasak nem-singleton classok eltárolására (lásd: van char, és char manager: char tartalmazza magát a játékos classt, a char manager meg létrehozza és eltárolja egy map-ban, így bárhonnan el tudjuk érni)) találunk is egy erre megfelelő kódot (jelen esetben ez nekünk az m_mapPC változó, ami a questmanager-ben van private-ben, tehát más classból el sem érjük), és innentől már sima ügy az egész.

    3. lépés: Akkor most már oldjuk meg...

    A gondolatmenet ismert. Csináljunk egy funkciót, ami végigmegy az összes játékoson (vagyis annak az említett változónak a tartalmán), majd állítsuk át a változónk tartalmát. Mivel az a változó egy másik class "gyermeke", így kénytelenek vagyunk funkciót írni hozzá (mivel private változó), hacsak nem találunk egy alkalmas már meglévő funkciót, jelen esetben a CancelRunning-ot.

    Ha megírtunk a funkciónkat, már csak azt kell kitalálni, hogy hol használjuk fel. Szerintem adja magát az ötlet: nyilván a reload funkció elejére kéne rakni...

    Éééés el is kezdhetünk tesztelni. Ha rajok vagyunk elsőre összejön (így volt ez velem is, mint a videóból látni). Vagyis hát majdnem... és itt jön a 4. lépés.

    4. lépés: Tegyük player-friendlyvé a dolgot...

    Habár a dolog működik, hiszen most már ha leellenőrizzük a dolgokat már nem crashelünk be, azonban ottmarad a jól ismert "mozis" nézet, és relog esetén tudjuk csak leszedni, vagy ha megnyitunk egy másik questet. És ugyebár ezt nem szeretnénk látni, mert igényes emberek vagyunk, tehát valamit ki kell találni.

    Jelen esetben ez elég egyszerűnek tűnik, küldjünk egy packetet a kliesnek, hogy menjen a fenébe a hiperszuper mozis nézetével ha éppen reloadozunk, meg vigye a ...-ba az éppen megnyitott questet, hiszen ha rányom a gombra a player max egy sysert kapunk tőle ajándékba, tehát semmi szükség erre az egészre.

    Feleslegesen ugyebár nem kell új paceketet meg funkciókat írni, van már egy meglévő, amin a többi questfunkció megy (milyen gomb kell), így azt ha kicsit megbuheráljuk, akkor jók is vagyunk. Innentől már csak egy kicsit ismerni kell a python c-apiját, hogy hogyan kell meghívni pythonos funkciót az indítóból (erre is van rengeteg példa a fájlokban egyébként), onnan meg már gyerekjáték, hiszen "csak" az összes questet be kell zárjuk, ami éppen meg van nyitva (ugyebár ismerős, hogy ha nyomkodjuk a lóhívót van hogy több is megnyílik, tehát az egész "dictionary"-t érdemes bezárnunk.




    Azt hiszem ezzel végeztünk is, remélem van akinek volt kedve ebből tanulni és elolvasni, ha további kérdés van ezzel kapcsolatban, vagy a fixxel kapcsolatban merül fel probléma, tessék valahol rám(s)írni.
    [/spoiler]
    EhPortal 1.39 © 2025, WebDev