Savovani a Loadovani
Z Moriawiki
← Starší verze | Novější verze →
Source(s): Savovani a Loadovani
Obsah |
Save soubory
Ultima bezi v persistentnim svete, a pro zajisteni persistence je treba savovat. To je asi celkem kazdymu jasny :) Savuje se do stejne jako v pripade phereserveru do textovych souboru, pri kazdym save se vytvori jeden pro kazdej existujici IBaseClassSaveCoordinator: v tuhle chvili to znamena accounts.sav, things.sav, regions.sav, a krome toho navic taky globals.sav.
Accounts.sav prekvapive obsahuje herni ucty... Oproti sphere je asi jedina vyznamna zmena to, ze pri odpovidajicim nastaveni v .ini se hesla savujou v hashovany podobe.
Do things.sav se savujou vsechny ingame objekty - postavy hracu, npc, itemy, a vsechny souvisejici objekty (Timery, Memory atd.). Zatimco sphere tady mela jen 2 mozny nazvy sekci (WORLDITEM a WORLDCHAR) tak Steamengine vsechno uklada podle jmena tridy, Item a Character je jen zacatek :) Z nich se dale dedi tridy jako Equippable, Container, Player, atd.
Regions.sav obsahuje regiony. Ty se nedefinujou ve skriptech jako u sphere, jsou jen v savech. Proto taky je jeden save na SVN, prave s tema regionama. V budoucnu pujde jednotlivy regiony importovat a exportovat z vyvojarskych instanci SE.
Globals.sav obsahuje instanci tridy Globals (coz je v podstate to co je ve sphere nazvano SERV, tj. reprezentace globalniho objektu), jejiz tagy jsou globalni promenny (tj. ve skriptu VAR), dale se tam pak ukladaj zmeny provedeny ze hry v AbstractDef objektech (tj. skriptovych definic itemu, charu, pluginu, skillu atd.) Tj. kdyz ve hre zmenite napriklad defaultni nazev nejakyho itemu (treba i_gold.Name = "penize"), tak se v globals.sav vytvori odpovidajici sekce (neco jako [ItemDef i_gold] a pod sebou bude mit onu zmenenou hodnotu). Tohle jsme castecne meli implementovany na Morii jako soucast .Nastaveni, moderni sphereservery 56+ to taky umej (tam se to uklada do nejakyho spheredata.scp nebo tak nejak).
Ty soubory jadro standartne prska proste do rootu adresare Saves. Nicmene to asi takhle neuvidite, protoze tohle chovani je upraveny existujicim skriptem (systems/backupManager.cs), kterej to uz dela inteligentnejc. V adresari Saves tvori podadresare podle dne ulozeni (napr. "2007-02-20") a v nich dalsi podadresare podle casu ("02_31_27"), kam teprve uklada ty trojice .sav souboru. V tom skriptu lze nastavit i limity kolik se ma udrzovat starych zalozh, a to bud podle celkovyho poctu, nebo podle zabranyho mista, nebo podle zbyvajiciho mista na disku. Kdyz se tyhle limity presahnou, tak se maze :) a maze se podle priorit ktery jsou dany jednak starim zalohy, a jednak "vyznamnosti" zalohy, tj. toho, jak velkej casovej usek pokryva. Tj. v sekvenci savu jdoucich za sebou po minute je jasny ze ty "prostredni" pujdou na smazani prvni ;) Krome toho tam lze zapnout, ze se pri savovani ta zaloha rovnou komprimuje zipem (a pri loadovani pak dekomprimuje), ovsem jestli tuhle featuru pouzije bude zalezet predevsim na tom jak rychle budou probihat savy nasich dvou milionu itemu, coz se da predem tezko odhadnout. Pokud by vas nekoho napadlo, jak ten BackupManager jeste vylepsit, dejte mi vedet, nebo se toho ujmete.
Pri loadovani muzu nastat nejaka kriticka chyba - typicky kdyz na konci souboru neni nalezeno [EOF] coz indikuje ze save nebyl uplnej. Pak se maze nahranej svet, a zacina se loadovat znova predchozi zaloha. Kdyz neexistuje zadna predchozi, je to fatal error a dal se nejede :)
Pravidelny savovani je zajistovany prostinkym skriptikem PeriodicSave.scp, kterej proste jednou za hodinu (nebo jinej nastavenej interval) pusti prikaz Globals.Save().
Source(s): Savovani a Loadovani
Pristup ve skriptech
Save i Load vyvolavaji odpovidajici globalni udalosti (tj. volaji @triggery na Globals.instance)
@beforeLoad - 1 parametr - Path typu String - vola se tesne pred zacatkem loadovani sveta, v parametru je cesta do adresare Saves, a do toho parametru lze priradit novou hodnotu (pouzito BackupManagerem), odkud se ma loadovat.
@openLoadStream - 2 parametry - Path typu String a Stream typu string (s ocekavanym navratovym typem Stream resp. TextReader) - vola se n-krat behem loadu, pro kazdej soubor jednou. Path je pro vsechny stejna a je shodna s tim co bylo vraceno v minulym triggeru, druhej parametr je nazev loadovanyho souboru bez pripony (tj. "things", "accounts", "globals"). Od skriptu implementujiciho tenhle trigger se ocekava ze pres druhej parametr vrati otevrenej soubor jako stream. Tj. je tu moznost prave pro komprimovanej stream, inmemory stream nebo nejakou jinou srandu.
@afterLoad - 2 parametr - Path typu String a Success typu Boolean - ukoncuje load fazi. druhej parametr indikuje jestli byl save uspesny. (V pripade ze nebyl, zacina se znova na starsim save, ze...)
Nutno dodat ze vsechny tyhle @...Load triggery se poustej predtim, nez je svet v "legalnim" stavu, takze se v techto triggerech pokud mozno nepokousejte pristupovat k objektum ze sveta. K tomu slouzi napriklad dalsi trigger:
@startup - bez parametru - nesouvisi primo s loadovanim/savovanim, ale pisu ho sem pro uplnost (neb se pouziva v PeriodicSave). Pousti se Tesne pred spustenim hlavni smycky, tj. od @afterLoad ho deli jeste nejaky ty inicializacni faze. V tuto chvili uz je svet v podstate funkcni.
@beforeSave - parametr Path typu String - podobne jako @beforeLoad, lze tu zmenit cestu kam soubory ukladat.
@openSaveStream - parametry Path typu String a Stream typu string (s ocekavanym navratovym typem Stream resp. TextWriter) - odpovidajici protejsek @openLoadStream, netreba se opakovat.
@afterSave - parametry Path typu String a Success tpu Boolean - Spousti se po save, druhy parametr je true pokud byl save uspesny, false pokud neuspesny.
Source(s): Savovani a Loadovani
Rozsiritelnost
Nebudu tu rozebirat mnozstvi kodu v jadru starajici se o save/load (je toho hodne :), ale zamerim se na to co vsechno lze savovat a jak pridavat dalsi moznosti. Pochopitelne v pripade rozsirovani o dalsi objekty se predpokladana nejaka znalost C# / objektoveho programovani.
Je zrejme, ze Steamengine umi savovat ingame objekty (item/char), accounty a globalni promenne. Do techto promennych a do promennych (a tagu) na tech charech/itemech lze ovsem ukladat dalsi objekty libovolnych typu, vzajemne vselijak provazanych, a je vetsinou zadouci aby i tyto objekty prezily reload. Ve steamengine je to, narozdil napriklad od sphere (a do znacne miry i treba narozdil od RunUO(!)), velice dobre mozne uskutecnit. Mnozina typu (trid) jejichz instance umi SteamEngine ukladat je pochopitelne konecna, ale libovolne rozsiritelna, a to o "navody k ukladani" jak vlastnich objektu, tak objektu vestavenych v .NET frameworku. Jakmile ma SE tento navod, pouzije ho na instanci dotycneho typu at je kdekoli - v tagu nejakeho charu/itemu, v globalni promenne, nebo jako promenna na jinem "user-defined" objektu. JInymi slovy, navod k ulozeni jednoho typu staci poskytnout jen jednou :)
Source(s): Savovani a Loadovani
Troska teorie
Obecne lze typy objektu rozdelit na 2 skupiny - takove u kterych zalezi na pametove ekvivalenci, a na ty u kterych nezalezi. To asi neni uplne srozumitelne, takze uvedu priklad:
Point4D, miniaturni trida ktera v sobe sdruzuje 4 ciselne souradnice. Jakmile je jeden Point vytvoren, souradnice v nem uz nelze zmenit (takovemu typu se mimochodem rika immutable), lze je jenom cist. Na jeden takovy Point se muze odkazovat vic ruznych objektu (ktere nejak souvisi s dotycnymi souradnicemi), a tyto ruzne objekty si mohou byt jisty ze jim nekdo jiny ty souradnice nezmeni - proste to nejde :) Pri vytvoreni je tento Point jedno misto v pameti kde jsou ulozeny ony 4 ciselne souradnice. Pri ulozeni a opetnem nahrani dotycnych objektu ktere se na tento Point odkazovaly si kazdy ten objekt ulozil a nahral "svuj vlastni" Point. Ted uz muze byt ten samy Point ulozen vickrat na vice mistech v pameti, na kazdy tento vyskyt odkazuje jen jeden objekt z tech drivejsich nekolika... Takze se zda ze je neco spatne, Reload neco zmenil... Ale ve skutecnosti to tak neni, vsechno funguje porad stejne, ze tech Pointu je vice je uplne jedno (akorat zabiraj trochu vic pameti), protoze porad odkazuji na stejne souradnice. Toto tedy znamena ze u Pointu nezalezi na pametove ekvivalenci.
Oproti tomu treba Item - komplikovana trida, ingame objekt. Vezmeme si priklad treba guildovniho kamenu - kazdej clen dotycny guildy ma kdesi ulozenej odkaz na tento jeden item. Kdyz jsou pak tito hraci ulozeni, mohl by si kazdy z nich ulozit i "svuj vlastni" guildstone. Po nahrani by pak vzniklo X samostatnych guild kazda s jednim hracem, nehlede na to ze by se ve svete mnozily itemy... Nerealne. Zde plati ze Musi byt bezpodminecne zachovany vsechny odkazy na dotycny Item z ostatnich objektu (zde hracu), tj. ze kdyz se pred savem vsichni hraci odkazujou na jedno misto v pameti kde je ulozen Guildstone, tak se musej na jedno misto odkazovat i po reloadu sveta. Jinymi slovy, u objektu typu Item zalezi na pametove ekvivalenci.
"Navod na savovani" objektu prvniho typu je ve Steamengine reprezentovan rozhranim ISimpleSaveImplementor, v pripade druheho typu je to ISaveImplementor.
Source(s): Savovani a Loadovani
Implementace v C#
ISimpleSaveImplementor obecne receno vytvari a cte retezce ktere se zapisuji do jednoho radku v sekci nadrizeneho objektu, vedle jmena dotycne promenne. Tedy napriklad retezce se ukladaji proste v uvozovkach (name="Gold Coin"), cisla jako...no, cisla :) (amount=500), casove udaje jako cisla prefixovana dvojteckou resp. dvema dvojteckami (Time=::633069247736875000), Pointy jako cisla oddelena carkami prefixovana "(2/3/4D)" (P=(4D)5233,31,20), atd. Ulozeny retezec musi mit unikatni format, ktery lze precist regularnim vyrazem. Tento regularni vyraz, stejne jako zaruka unikatnosti, je na autorovi dotycne ISimpleSaveImplementor tridy. Prikladu lze najit nekolik v jadre SE, a predevsim ve skriptech (classes\saving).
ISaveImplementor vytvari a cte cele sekce v save, tj neco co zacina hlavickou [nazevtridy unikatnicislo] a pokracuje mnozstvim radku typu nazev=hodnota. Nekolik prikladu techto implementoru je opet ve skriptech, tykaji se nekolika vestavenych .NET kolekci (tj. pole - Array, dynamicke pole - ArrayList, genericke dynamicke pole - List<>, a hashova tabulka - Hashtable). Pro vlastni tridy je tu ovsem daleko pohodlnejsi moznost nez psat vlastni ISaveImplementor (kterej muze bejt dost slozitej) - vzit definici tridy jejiz instance chci ukladat, a "ozdobit" ji tzv. atributama (to s tim ozdobenim sem si nevymyslel, v pripade atributu se v .NET svete bezne mluvi o "decorating", proto sem taky tridu ktera tuhle funkcionalitu podporuje nazval DecoratedClassSaveImplementor :) Jak na to?
Nejdriv je treba rici co je to Atribut - je to jakasi miniaturni trida, jejiz instance se prirazujou jinym tridam a jejich clenum. Prirazujou se behem kompilace, a v C# zdrojacich se prirazujou pomoci napsani nazvu atributu (bez koncovky Attribute) do hranatych zavorek nad deklaraci entity kterou timto atributem dekorujete. Takze rekneme trida GMPageEntry s atributem SaveableClassAttribute bude vypadat takhle
[SaveableClass] public class GMPageEntry { ... }
Nuze, pomoci nekolika atributu lze oznacit tridu ktera se ma savovat, a jeji cleny, ktere se maji brat v uvahu. Nejdriv je trba dat vedet ze vubec dotycnou tridu chceme urcit k tomu aby byla savovana, to se deje prave prirazenim vyse zmineneho atributu SaveableClass k samotne deklaraci tridy. Timto si otevreme moznost k pouziti ostatnich atributu, ktere poskytuji prave ten "navod". Prvni vec co je treba urcit je konstruktor nebo staticka metoda, ktera vraci novou instanci dotycne tridy. Tento konstruktor resp. staticka metoda nebude mit zadne parametry, a bude mit navratovy typ prave dotycnou tridu (konstruktor ani jiny typ mit nemuze). Tuto metodu resp. konstruktor je treba oznacit atributem LoadingInitializer.
[LoadingInitializer] public GMPageEntry() { } //ten konstruktor bude bezne proste prazdny, bez kodu, protoze naplneni hodnot teprve nastane
Potom je treba oznacit vsechny public Fieldy (promenne) nebo Property, ktere chceme savovat. Tyto musi byt znamych typu, znamych v tom smyslu ze jsou to bud primitivni typy (cisla/stringy) nebo ingame objekty, nebo na ne existuje nekde jinde SaveImplementor. Tyto budiz oznaceny atributem SaveableData.
[SaveableData] public AbstractCharacter Sender; //player who has sent the page [SaveableData] public string Reason; //the players description of the problem [SaveableData] public Point4D P; //location where the page was posted [SaveableData] public DateTime Time; //time when the page was posted
No, a to je v mnohych pripadech vsechno :) Timto zajistime ze instance tridy GMPageEntry se budou ukladat jako jedinecne objekty, a budou se zachovavat hodnoty 4 jejich promennych.
Jsou tu pak jeste moznosti "pro pokrocile":
LoadLineAttribute oznacuje metodu se ctyrmi parametry - string filename, int line, string prop, string value - tj. jmeno souboru, cislo radku, nazev hodnoty, "hodnota hodnoty" resp. jeji textova reprezentace tak jak se precetla ze save souboru. Pri pouziti tohoto atributu se od oznacene metody ocekava ze nahraje vsechny hodnoty ktere nelze z ruznych duvodu nahravat primo pres [SaveableData]. Tato metoda se bude volat opakovane, pro kazdy radek sekce (tedy radek ktery nebyl zatim pouzit pro [SaveableData]).
SaveAttribute oznacuje metodu s jednim parametrem typu SaveStream, coz je "chytrej" stream pro zapis novych sekci a radku "nazev=hodnota". Pri pouziti tohoto atributu se od oznacene metody ocekava ze ulozi vsechny hodnoty ktere nelze z ruznych duvodu ukladat primo pres [SaveableData]. Tedy pri pouziti [Save] se logicky ocekava ze bude pouzita i [LoadLine] nebo [LoadSection] (viz nize)
LoadSectionAttribute oznacuje static metody/konstruktory vracejici 'kompletni' instanci. Parametrem teto tridy je instance typu PropsSection, coz je trida ktera v SE reprezentuje nactenou sekci ze save souboru, tj. lze pomoci ni jednoduse pristupovat k stringum v hlavicce a jednotlivych radcich "nazev=hodnota". V pripade pouziti [LoadSection] se nepouzivaji atributy [LoadingInitializer], [SaveableData] ani [LoadLine] - proste se predpoklada ze vsechnu praci provede tato metoda.
Nutno jeste dodat ze vsechny ISaveImplementor a ISimpleSaveImplementor tridy jsou ze sve podstaty singletony, a jsou inicializovany automaticky jadrem, pomoci defaultnich (tedy bezparametrovych public) konstruktoru. Toto plati i v pripade implementoru vzniklych z dekorovanych trid, ktere se generuji do souboru bin\DecoratedClassesSaveImplementors.Generated.cs (ktery je mimochodem v projektovych souborech Visual Studia prilinkovan jako soucast projektu Scripts, i kdyz se ve skutecnosti kompiluje az po kompilaci skriptu. Ale je to jedno nebot steamengine se stejne kompiluje mimo Visual Studio). Pokud jste tomuto (nebo predchozim) odstavci nerozumeli, nic si z toho nedelejte, proste se drzte prikladu v existujicich skriptech, ktere najdete v Firewall.cs, GMPage.cs, Memory.cs, a dalsich.
IBaseClassSaveCoordinator - rozhrani ktere se pouziva (a patrne to tak zustane) jen v jadru. Kazda jeho implementace odpovida jednomu typu ukladanych objektu. Vsechny maji spolecne to, ze instance tech trid jsou nejak globalne jednoznacne identifikovatelne. V tuhle chvili existuje implementace pro Thing (identifikace pres unikatni uid cislo), Region a AbstractAccount (identifikace pres unikatni jmeno).
Source(s): Savovani a Loadovani
Ukladani statickych promennych
Vyse receno bylo, kterak vyrobit navod k ulozeni instanci trid. Aby se ovsem instance tridy vubec zacala ukladat, musi byt nejakym zpusobem soucasti entit ktere se z definice do savu ukladaji - tedy accountu, charu/itemu nebo Globals. Toto lze dosahnout bud tak ze dotycnou instanci strcim do nejake globalni promenne (tedy tagu na Globals.instance), v mnohych pripadech to ale neni prilis elegantni. Soucasi mnohych systemu je seznam objektu ktere nemaji se svetem jako takovym temer nic spolecneho, a vyhlasovat je za globalni promenne je nepohodlne.
Proto existuje cesta jak oznacit staticky Field (promennou) nebo statickou Property (pricemz nemusi byt public, muze byt i private) tak aby se jejich hodnota automaticky ukladala a opet vkladala na dotycne misto. Takovou promennou/property je treba oznacit atributem [SavedMember], a tridu ve ktery se nachazi atributem [HasSavedMembers]. Priklady lze opet nalezt ve skriptech GMPage.cs, Firewall.cs, atd.
Nutno dodat ze samotna tato fukncionalita je realizovana skriptem (StaticMemberSaver.cs), z cehoz mimo jine vyplyva ze pripadna chybova hlaseni nebudou uplne prekrasny a jednoznacny, mozna nebudou obsahovat jmena souboru a cisla radku, atd.
Source(s): Savovani a Loadovani