dokumentation 12 seiten
This commit is contained in:
parent
13154fbb53
commit
3a09a7285e
@ -37,13 +37,32 @@
|
|||||||
\@writefile{toc}{\contentsline {subsection}{\numberline {3.1}Vor- und Nachteile des Key-Value-Modells}{8}{subsection.3.1}\protected@file@percent }
|
\@writefile{toc}{\contentsline {subsection}{\numberline {3.1}Vor- und Nachteile des Key-Value-Modells}{8}{subsection.3.1}\protected@file@percent }
|
||||||
\@writefile{toc}{\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}\protected@file@percent }
|
\@writefile{toc}{\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}\protected@file@percent }
|
||||||
\@writefile{toc}{\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}\protected@file@percent }
|
\@writefile{toc}{\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}\protected@file@percent }
|
||||||
\@writefile{tdo}{\contentsline {todo}{Wir koennten Regionen Traversierung Pathfinding einbauen}{8}{section*.1}\protected@file@percent }
|
\BKM@entry{id=13,dest={73756273656374696F6E2E332E34},srcline={340}}{5C3337365C3337375C303030465C303030615C3030307A5C303030695C30303074}
|
||||||
\pgfsyspdfmark {pgfid1}{19562756}{23935922}
|
\@writefile{toc}{\contentsline {subsection}{\numberline {3.4}Fazit}{9}{subsection.3.4}\protected@file@percent }
|
||||||
\@writefile{tdo}{\contentsline {todo}{Wir haben keine Beziehungen, die ueber mehrere Knoten hinweg gehen. Keine komlexen beziehungen wo der graph einen hohen nutzen mit sich bringt}{8}{section*.2}\protected@file@percent }
|
\BKM@entry{id=14,dest={73656374696F6E2E34},srcline={368}}{5C3337365C3337375C3030304D5C303030615C303030725C3030306B5C303030745C303030725C303030655C303030635C303030685C303030655C303030725C303030635C303030685C30303065}
|
||||||
\pgfsyspdfmark {pgfid2}{19562756}{18172368}
|
\BKM@entry{id=15,dest={73756273656374696F6E2E342E31},srcline={374}}{5C3337365C3337375C303030425C303030655C303030745C303030725C303030615C303030635C303030685C303030745C303030655C303030745C303030655C3030305C3034305C303030445C303030615C303030745C303030655C3030306E5C303030625C303030615C3030306E5C3030306B5C303030655C3030306E}
|
||||||
\BKM@entry{id=13,dest={73656374696F6E2E34},srcline={340}}{5C3337365C3337375C303030465C303030615C3030307A5C303030695C30303074}
|
\BKM@entry{id=16,dest={73756273756273656374696F6E2E342E312E31},srcline={376}}{5C3337365C3337375C303030525C303030655C303030645C303030695C30303073}
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {4}Fazit}{9}{section.4}\protected@file@percent }
|
\BKM@entry{id=17,dest={73756273756273656374696F6E2E342E312E32},srcline={384}}{5C3337365C3337375C3030304D5C303030655C3030306D5C303030635C303030615C303030635C303030685C303030655C30303064}
|
||||||
|
\BKM@entry{id=18,dest={73756273756273656374696F6E2E342E312E33},srcline={392}}{5C3337365C3337375C303030415C303030725C303030615C3030306E5C303030675C3030306F5C303030445C303030425C3030305C3034305C3030305C3035305C3030306D5C303030755C3030306C5C303030745C303030695C3030306D5C3030306F5C303030645C303030615C3030306C5C3030305C303531}
|
||||||
|
\BKM@entry{id=19,dest={73756273756273656374696F6E2E342E312E34},srcline={401}}{5C3337365C3337375C303030415C3030306D5C303030615C3030307A5C3030306F5C3030306E5C3030305C3034305C303030445C303030795C3030306E5C303030615C3030306D5C3030306F5C303030445C30303042}
|
||||||
|
\@writefile{toc}{\contentsline {section}{\numberline {4}Marktrecherche}{10}{section.4}\protected@file@percent }
|
||||||
|
\@writefile{toc}{\contentsline {subsection}{\numberline {4.1}Betrachtete Datenbanken}{10}{subsection.4.1}\protected@file@percent }
|
||||||
|
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.1}Redis}{10}{subsubsection.4.1.1}\protected@file@percent }
|
||||||
|
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.2}Memcached}{10}{subsubsection.4.1.2}\protected@file@percent }
|
||||||
|
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.3}ArangoDB (multimodal)}{10}{subsubsection.4.1.3}\protected@file@percent }
|
||||||
|
\BKM@entry{id=20,dest={73756273756273656374696F6E2E342E312E35},srcline={410}}{5C3337365C3337375C303030655C303030745C303030635C30303064}
|
||||||
|
\BKM@entry{id=21,dest={73756273756273656374696F6E2E342E312E36},srcline={416}}{5C3337365C3337375C303030525C303030695C303030615C3030306B5C3030305C3034305C3030304B5C30303056}
|
||||||
|
\BKM@entry{id=22,dest={73756273656374696F6E2E342E32},srcline={422}}{5C3337365C3337375C3030304E5C303030755C303030745C3030307A5C303030775C303030655C303030725C303030745C303030615C3030306E5C303030615C3030306C5C303030795C303030735C30303065}
|
||||||
|
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.4}Amazon DynamoDB}{11}{subsubsection.4.1.4}\protected@file@percent }
|
||||||
|
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.5}etcd}{11}{subsubsection.4.1.5}\protected@file@percent }
|
||||||
|
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.6}Riak KV}{11}{subsubsection.4.1.6}\protected@file@percent }
|
||||||
|
\@writefile{toc}{\contentsline {subsection}{\numberline {4.2}Nutzwertanalyse}{11}{subsection.4.2}\protected@file@percent }
|
||||||
|
\@writefile{lot}{\contentsline {table}{\numberline {4}{\ignorespaces Nutzwertanalyse der sechs Key-Value-Datenbanken}}{11}{table.4}\protected@file@percent }
|
||||||
|
\newlabel{tab:nutzwertanalyse}{{4}{11}{Nutzwertanalyse der sechs Key-Value-Datenbanken}{table.4}{}}
|
||||||
|
\BKM@entry{id=23,dest={73756273656374696F6E2E342E33},srcline={457}}{5C3337365C3337375C303030445C303030615C303030745C303030655C3030306E5C303030625C303030615C3030306E5C3030306B5C303030615C303030755C303030735C303030775C303030615C303030685C3030306C}
|
||||||
|
\@writefile{toc}{\contentsline {subsection}{\numberline {4.3}Datenbankauswahl}{12}{subsection.4.3}\protected@file@percent }
|
||||||
\global\@namedef{scr@dte@section@lastmaxnumwidth}{12.14694pt}
|
\global\@namedef{scr@dte@section@lastmaxnumwidth}{12.14694pt}
|
||||||
\global\@namedef{scr@dte@subsection@lastmaxnumwidth}{19.70898pt}
|
\global\@namedef{scr@dte@subsection@lastmaxnumwidth}{19.70898pt}
|
||||||
|
\global\@namedef{scr@dte@subsubsection@lastmaxnumwidth}{28.84563pt}
|
||||||
\@writefile{toc}{\providecommand\tocbasic@end@toc@file{}\tocbasic@end@toc@file}
|
\@writefile{toc}{\providecommand\tocbasic@end@toc@file{}\tocbasic@end@toc@file}
|
||||||
\gdef \@abspage@last{9}
|
\gdef \@abspage@last{12}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
# Fdb version 4
|
# Fdb version 4
|
||||||
["pdflatex"] 1780313071.59521 "/home/hikyu/Documents/Uni/2026 Sommer/Moderne Datenbanken/Ringstats_Dokumentation.tex" "Ringstats_Dokumentation.pdf" "Ringstats_Dokumentation" 1780313072.62514 0
|
["pdflatex"] 1783431678.04028 "/home/hikyu/Documents/Uni/2026 Sommer/Moderne Datenbanken/Ringstats_Dokumentation.tex" "Ringstats_Dokumentation.pdf" "Ringstats_Dokumentation" 1783431679.12842 0
|
||||||
"/home/hikyu/Documents/Uni/2026 Sommer/Moderne Datenbanken/Ringstats_Dokumentation.tex" 1780313071.55337 16584 03bd9bb6b9242fada2b5fd161766d7ed ""
|
"/home/hikyu/Documents/Uni/2026 Sommer/Moderne Datenbanken/Ringstats_Dokumentation.tex" 1783431677.99026 22127 9c02fde45b05485b4d6e44f2a7e8ba1e ""
|
||||||
"/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-t1.enc" 1775415801 2971 def0b6c1f0b107b3b936def894055589 ""
|
"/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-t1.enc" 1775415801 2971 def0b6c1f0b107b3b936def894055589 ""
|
||||||
|
"/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-ts1.enc" 1775415801 2900 1537cc8184ad1792082cd229ecc269f4 ""
|
||||||
"/usr/share/texmf-dist/fonts/map/fontname/texfonts.map" 1775415801 3524 cb3e574dea2d1052e39280babc910dc8 ""
|
"/usr/share/texmf-dist/fonts/map/fontname/texfonts.map" 1775415801 3524 cb3e574dea2d1052e39280babc910dc8 ""
|
||||||
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1095.tfm" 1775415801 3584 21b378cca2e40816b0e6d74a4dc98f04 ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm" 1775415801 3584 402da0b29eafbad07963b1224b222f18 ""
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm" 1775415801 3584 402da0b29eafbad07963b1224b222f18 ""
|
||||||
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1095.tfm" 1775415801 3584 929cdff2b7a8c11bd4d49fd68cb0ae70 ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1200.tfm" 1775415801 3584 f80ddd985bd00e29e9a6047ebd9d4781 ""
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1200.tfm" 1775415801 3584 f80ddd985bd00e29e9a6047ebd9d4781 ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1728.tfm" 1775415801 3584 3c76ccb63eda935a68ba65ba9da29f1a ""
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1728.tfm" 1775415801 3584 3c76ccb63eda935a68ba65ba9da29f1a ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecss1200.tfm" 1775415801 3584 db562fb1d1c474cc55f0ce8a1dd94fe7 ""
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecss1200.tfm" 1775415801 3584 db562fb1d1c474cc55f0ce8a1dd94fe7 ""
|
||||||
@ -12,6 +15,8 @@
|
|||||||
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecsx1728.tfm" 1775415801 3584 a5b048a4e362640a1d2d34e624589d34 ""
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecsx1728.tfm" 1775415801 3584 a5b048a4e362640a1d2d34e624589d34 ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecsx2488.tfm" 1775415801 3584 8530f700a3778253a0a6d6639fda16c7 ""
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecsx2488.tfm" 1775415801 3584 8530f700a3778253a0a6d6639fda16c7 ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ectt1200.tfm" 1775415801 1536 487c9b46984a816c7ed238d0674595c7 ""
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ectt1200.tfm" 1775415801 1536 487c9b46984a816c7ed238d0674595c7 ""
|
||||||
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/tcrm1200.tfm" 1775415801 1536 74b7293ec3713bb7fdca8dd1bd1f469c ""
|
||||||
|
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi10.tfm" 1775415801 1528 abec98dbc43e172678c11b3b9031252a ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi12.tfm" 1775415801 1524 4414a8315f39513458b80dfc63bff03a ""
|
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi12.tfm" 1775415801 1524 4414a8315f39513458b80dfc63bff03a ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm" 1775415801 1512 f21f83efb36853c0b70002322c1ab3ad ""
|
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm" 1775415801 1512 f21f83efb36853c0b70002322c1ab3ad ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm" 1775415801 1520 eccf95517727cb11801f4f1aee3a21b4 ""
|
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm" 1775415801 1520 eccf95517727cb11801f4f1aee3a21b4 ""
|
||||||
@ -23,7 +28,9 @@
|
|||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm" 1775415801 1124 6c73e740cf17375f03eec0ee63599741 ""
|
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm" 1775415801 1124 6c73e740cf17375f03eec0ee63599741 ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1775415801 1116 933a60c408fc0a863a92debe84b2d294 ""
|
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1775415801 1116 933a60c408fc0a863a92debe84b2d294 ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1775415801 1120 8b7d695260f3cff42e636090a8002094 ""
|
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1775415801 1120 8b7d695260f3cff42e636090a8002094 ""
|
||||||
|
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1095.pfb" 1775415801 154600 ea54091d31de803b613ba9e80ca51709 ""
|
||||||
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb" 1775415801 140176 d4962f948b4cc0adf4d3dde77a128c95 ""
|
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb" 1775415801 140176 d4962f948b4cc0adf4d3dde77a128c95 ""
|
||||||
|
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1095.pfb" 1775415801 145929 f25e56369a345c4ff583b067cd87ce8e ""
|
||||||
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1200.pfb" 1775415801 136101 f533469f523533d38317ab5729d00c8a ""
|
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1200.pfb" 1775415801 136101 f533469f523533d38317ab5729d00c8a ""
|
||||||
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1728.pfb" 1775415801 131438 3aa300b3e40e5c8ba7b4e5c6cebc5dd6 ""
|
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1728.pfb" 1775415801 131438 3aa300b3e40e5c8ba7b4e5c6cebc5dd6 ""
|
||||||
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1200.pfb" 1775415801 148436 6a185415b8a1b77c58fee2272f3574e2 ""
|
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1200.pfb" 1775415801 148436 6a185415b8a1b77c58fee2272f3574e2 ""
|
||||||
@ -160,11 +167,11 @@
|
|||||||
"/usr/share/texmf-dist/tex/latex/xcolor/xcolor.sty" 1775415801 55384 b454dec21c2d9f45ec0b793f0995b992 ""
|
"/usr/share/texmf-dist/tex/latex/xcolor/xcolor.sty" 1775415801 55384 b454dec21c2d9f45ec0b793f0995b992 ""
|
||||||
"/usr/share/texmf-dist/tex/latex/xkeyval/xkeyval.sty" 1775415801 5006 d7e899bb5d9717c7f214e9bc0284de7b ""
|
"/usr/share/texmf-dist/tex/latex/xkeyval/xkeyval.sty" 1775415801 5006 d7e899bb5d9717c7f214e9bc0284de7b ""
|
||||||
"/usr/share/texmf-dist/web2c/texmf.cnf" 1775415801 43569 fd570f2fa160877d211e859f687312ba ""
|
"/usr/share/texmf-dist/web2c/texmf.cnf" 1775415801 43569 fd570f2fa160877d211e859f687312ba ""
|
||||||
"/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map" 1777228096 5385779 47c0c393a77fd4a098bacfd57d66b88a ""
|
"/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map" 1780436122 5385779 47c0c393a77fd4a098bacfd57d66b88a ""
|
||||||
"/var/lib/texmf/web2c/pdftex/pdflatex.fmt" 1778762763 3734533 c370cc050c4a9823a1fe1ab265afc57b ""
|
"/var/lib/texmf/web2c/pdftex/pdflatex.fmt" 1780436120 3734532 282dcc45f1c728134b92f993ed6db952 ""
|
||||||
"Ringstats_Dokumentation.aux" 1780313072.58542 8525 dc2f45dff6b0575fd760f48e262e0d7d "pdflatex"
|
"Ringstats_Dokumentation.aux" 1783431679.08329 11920 6f4c4930e4f31ec828a5b3aab7f8ac7d "pdflatex"
|
||||||
"Ringstats_Dokumentation.tex" 1780313071.55337 16584 03bd9bb6b9242fada2b5fd161766d7ed ""
|
"Ringstats_Dokumentation.tex" 1783431677.99026 22127 9c02fde45b05485b4d6e44f2a7e8ba1e ""
|
||||||
"Ringstats_Dokumentation.toc" 1780313072.58642 1305 1401ae3dd1fbee665ef3e10a49daf4b1 "pdflatex"
|
"Ringstats_Dokumentation.toc" 1783431679.08529 2164 0e5aae9731a2850f18801d399dcfe537 "pdflatex"
|
||||||
"assets/er-modell.png" 1778764597.46144 805989 0b7323ca4b38e6518c24fd9e5684655b ""
|
"assets/er-modell.png" 1778764597.46144 805989 0b7323ca4b38e6518c24fd9e5684655b ""
|
||||||
(generated)
|
(generated)
|
||||||
"Ringstats_Dokumentation.aux"
|
"Ringstats_Dokumentation.aux"
|
||||||
|
|||||||
@ -276,8 +276,17 @@ INPUT ./assets/er-modell.png
|
|||||||
INPUT ./assets/er-modell.png
|
INPUT ./assets/er-modell.png
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm
|
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ectt1200.tfm
|
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ectt1200.tfm
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1095.tfm
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmr10.tfm
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmmi10.tfm
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1095.tfm
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/tcrm1200.tfm
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-ts1.enc
|
||||||
INPUT Ringstats_Dokumentation.aux
|
INPUT Ringstats_Dokumentation.aux
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1095.pfb
|
||||||
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb
|
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb
|
||||||
|
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1095.pfb
|
||||||
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1200.pfb
|
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1200.pfb
|
||||||
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1728.pfb
|
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1728.pfb
|
||||||
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1200.pfb
|
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1200.pfb
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026/Arch Linux) (preloaded format=pdflatex 2026.5.14) 1 JUN 2026 13:24
|
This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026/Arch Linux) (preloaded format=pdflatex 2026.6.2) 7 JUL 2026 15:41
|
||||||
entering extended mode
|
entering extended mode
|
||||||
restricted \write18 enabled.
|
restricted \write18 enabled.
|
||||||
%&-line parsing enabled.
|
%&-line parsing enabled.
|
||||||
@ -725,7 +725,7 @@ LaTeX Font Info: External font `cmex10' loaded for size
|
|||||||
{/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map}{/usr/share/texmf-dist/fonts
|
{/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map}{/usr/share/texmf-dist/fonts
|
||||||
/enc/dvips/cm-super/cm-super-t1.enc}]
|
/enc/dvips/cm-super/cm-super-t1.enc}]
|
||||||
[2]
|
[2]
|
||||||
<assets/er-modell.png, id=48, 3968.8275pt x 4311.10625pt>
|
<assets/er-modell.png, id=68, 3968.8275pt x 4311.10625pt>
|
||||||
File: assets/er-modell.png Graphic file (type png)
|
File: assets/er-modell.png Graphic file (type png)
|
||||||
<use assets/er-modell.png>
|
<use assets/er-modell.png>
|
||||||
Package pdftex.def Info: assets/er-modell.png used on input line 87.
|
Package pdftex.def Info: assets/er-modell.png used on input line 87.
|
||||||
@ -740,44 +740,37 @@ Overfull \hbox (9.63942pt too wide) in paragraph at lines 296--308
|
|||||||
[][]
|
[][]
|
||||||
[]
|
[]
|
||||||
|
|
||||||
[7]
|
[7] [8] [9] [10]
|
||||||
Package hyperref Info: bookmark level for unknown todo defaults to 0 on input l
|
LaTeX Font Info: External font `cmex10' loaded for size
|
||||||
ine 326.
|
(Font) <10.95> on input line 429.
|
||||||
|
[11] [12{/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-ts1.enc}] (./
|
||||||
Overfull \hbox (11.24713pt too wide) in paragraph at lines 326--326
|
Ringstats_Dokumentation.aux)
|
||||||
[]
|
|
||||||
[]
|
|
||||||
|
|
||||||
|
|
||||||
Overfull \hbox (11.24713pt too wide) in paragraph at lines 331--331
|
|
||||||
[]
|
|
||||||
[]
|
|
||||||
|
|
||||||
[8] [9] (./Ringstats_Dokumentation.aux)
|
|
||||||
***********
|
***********
|
||||||
LaTeX2e <2025-11-01>
|
LaTeX2e <2025-11-01>
|
||||||
L3 programming layer <2026-01-19>
|
L3 programming layer <2026-01-19>
|
||||||
***********
|
***********
|
||||||
)
|
)
|
||||||
Here is how much of TeX's memory you used:
|
Here is how much of TeX's memory you used:
|
||||||
24546 strings out of 469462
|
24519 strings out of 469462
|
||||||
479912 string characters out of 5468614
|
478765 string characters out of 5468615
|
||||||
1009471 words of memory out of 5000000
|
1015471 words of memory out of 5000000
|
||||||
52828 multiletter control sequences out of 15000+600000
|
52785 multiletter control sequences out of 15000+600000
|
||||||
638939 words of font info for 63 fonts, out of 8000000 for 9000
|
641744 words of font info for 69 fonts, out of 8000000 for 9000
|
||||||
14 hyphenation exceptions out of 8191
|
14 hyphenation exceptions out of 8191
|
||||||
108i,8n,108p,11313b,869s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
108i,8n,108p,11313b,463s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||||
</usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb></usr/share/t
|
</usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1095.pfb></usr/share/t
|
||||||
exmf-dist/fonts/type1/public/cm-super/sfrm1200.pfb></usr/share/texmf-dist/fonts
|
exmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb></usr/share/texmf-dist/fonts
|
||||||
/type1/public/cm-super/sfrm1728.pfb></usr/share/texmf-dist/fonts/type1/public/c
|
/type1/public/cm-super/sfrm1095.pfb></usr/share/texmf-dist/fonts/type1/public/c
|
||||||
m-super/sfsx1200.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx144
|
m-super/sfrm1200.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm172
|
||||||
0.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1728.pfb></usr/sha
|
8.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1200.pfb></usr/sha
|
||||||
re/texmf-dist/fonts/type1/public/cm-super/sfsx2488.pfb></usr/share/texmf-dist/f
|
re/texmf-dist/fonts/type1/public/cm-super/sfsx1440.pfb></usr/share/texmf-dist/f
|
||||||
onts/type1/public/cm-super/sftt1200.pfb>
|
onts/type1/public/cm-super/sfsx1728.pfb></usr/share/texmf-dist/fonts/type1/publ
|
||||||
Output written on Ringstats_Dokumentation.pdf (9 pages, 608048 bytes).
|
ic/cm-super/sfsx2488.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sft
|
||||||
|
t1200.pfb>
|
||||||
|
Output written on Ringstats_Dokumentation.pdf (12 pages, 668797 bytes).
|
||||||
PDF statistics:
|
PDF statistics:
|
||||||
172 PDF objects out of 1000 (max. 8388607)
|
252 PDF objects out of 1000 (max. 8388607)
|
||||||
141 compressed objects within 2 object streams
|
212 compressed objects within 3 object streams
|
||||||
29 named destinations out of 1000 (max. 500000)
|
41 named destinations out of 1000 (max. 500000)
|
||||||
122 words of extra memory for PDF output out of 10000 (max. 10000000)
|
202 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -337,7 +337,7 @@ Für dieses einfache Schreiben und Lesen bringt der Graph mehr Aufwand mit, als
|
|||||||
% \section{Bau der app bis zur Endpräsi}
|
% \section{Bau der app bis zur Endpräsi}
|
||||||
\newpage
|
\newpage
|
||||||
|
|
||||||
\section{Fazit}
|
\subsection{Fazit}
|
||||||
% Mehr coole sachen die wir gelernt haben.
|
% Mehr coole sachen die wir gelernt haben.
|
||||||
% Warum wir uns fuer dieses Datenbankmodell entschieden haben.
|
% Warum wir uns fuer dieses Datenbankmodell entschieden haben.
|
||||||
Wir haben die gleichen Daten aus unserem ER-Modell in drei Datenbanken abgebildet und dabei gesehen,
|
Wir haben die gleichen Daten aus unserem ER-Modell in drei Datenbanken abgebildet und dabei gesehen,
|
||||||
@ -363,4 +363,113 @@ Genau dafür ist Redis gemacht.
|
|||||||
Die komplexen Abfragen, bei denen MongoDB oder Neo4j stark wären, brauchen wir kaum,
|
Die komplexen Abfragen, bei denen MongoDB oder Neo4j stark wären, brauchen wir kaum,
|
||||||
und unsere Daten sind ohnehin nicht tief verschachtelt.
|
und unsere Daten sind ohnehin nicht tief verschachtelt.
|
||||||
Deshalb passt die schnelle und einfache Lösung am besten zu dem, was Ringstats braucht.
|
Deshalb passt die schnelle und einfache Lösung am besten zu dem, was Ringstats braucht.
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
\section{Marktrecherche}
|
||||||
|
In der Zwischenpräsentation haben wir uns für das Key-Value-Modell entschieden.
|
||||||
|
In diesem Kapitel schauen wir uns den Markt für Key-Value-Datenbanken genauer an.
|
||||||
|
Wir stellen sechs Kandidaten vor, darunter ArangoDB eine multimodale Datenbank,
|
||||||
|
und führen anschließend eine Nutzwertanalyse durch.
|
||||||
|
|
||||||
|
\subsection{Betrachtete Datenbanken}
|
||||||
|
|
||||||
|
\subsubsection{Redis}
|
||||||
|
Redis ist ein In-Memory Key-Value Store und unsere Wahl aus der Zwischenpräsentation.
|
||||||
|
Alle Daten liegen im Arbeitsspeicher, wodurch Lese- und Schreibzugriffe im Bereich
|
||||||
|
von Mikrosekunden liegen. Neben einfachen Strings bringt Redis Datentypen wie
|
||||||
|
Hashes und Sets mit, auf denen unser Schlüsselaufbau aus
|
||||||
|
Kapitel~2.2 direkt aufsetzt. Seit Version 8 steht Redis wieder unter einer Open-Source-Lizenz (AGPLv3)
|
||||||
|
und kostet uns damit nichts.
|
||||||
|
|
||||||
|
\subsubsection{Memcached}
|
||||||
|
Memcached ist eine verteilte In-Memory-Cache Datenbank und in Sachen
|
||||||
|
Geschwindigkeit vergleichbar mit Redis. Dafür ist der Funktionsumfang bewusst
|
||||||
|
klein gehalten: Es gibt nur einfache Key-Value-Paare, keine Datentypen wie Sets
|
||||||
|
oder Hashes, und keine Persistenz. Unser Schlüsselaufbau mit
|
||||||
|
SETs für Ausrüstung und Regionen lässt sich also nicht direkt abbilden
|
||||||
|
Memcached steht unter der BSD-Lizenz.
|
||||||
|
|
||||||
|
\subsubsection{ArangoDB (multimodal)}
|
||||||
|
ArangoDB ist eine multimodale Datenbank und vereint Dokumente, Graphen und
|
||||||
|
Key-Value-Zugriffe in einem System mit der gemeinsamen Abfragesprache AQL.
|
||||||
|
Damit hätten wir unsere drei Modellierungen aus Kapitel~2 theoretisch in einer
|
||||||
|
einzigen Datenbank abbilden können. Der Nachteil ist der Langsame zugriff auf die Daten.
|
||||||
|
Seit Version 3.12 steht ArangoDB unter der Business Source
|
||||||
|
License. Für unser Uni-Projekt ist das kostenlos, für einen
|
||||||
|
kommerziellen Betrieb gäbe es Einschränkungen.
|
||||||
|
|
||||||
|
\subsubsection{Amazon DynamoDB}
|
||||||
|
DynamoDB ist der verwaltete Key-Value- und Dokument-Store von AWS. Die Datenbank
|
||||||
|
skaliert beliebig. Der große Haken ist das Preismodell:
|
||||||
|
DynamoDB läuft nur in der AWS-Cloud und wird pro Anfrage und Speicher
|
||||||
|
abgerechnet. Bei unseren dauerhaften Schreibzugriffen von bis zu 10 Anfragen
|
||||||
|
pro Sekunde würden laufende Kosten entstehen, womit unser
|
||||||
|
Muss-Kriterium \glqq keine Lizenzkosten\grqq{} verletzt ist. Zudem ist die
|
||||||
|
Latenz deutlich höher durch den Netzwerkweg in die Cloud.
|
||||||
|
|
||||||
|
\subsubsection{etcd}
|
||||||
|
etcd ist ein Key-Value Store, der hauptsächlich zum Speichern von
|
||||||
|
Einstellungen und Konfigurationen benutzt wird, zum Beispiel intern von
|
||||||
|
Kubernetes, ist also nicht dafür gedacht, laufend Spielerdaten zu speichern wie bei uns.
|
||||||
|
Die Lizenz ist Apache~2.0 und damit kostenlos.
|
||||||
|
|
||||||
|
\subsubsection{Riak KV}
|
||||||
|
Riak KV ist ein verteilter Key-Value Store.
|
||||||
|
Seine Stärke liegt in der Ausfallsicherheit: Daten werden
|
||||||
|
automatisch über mehrere Knoten repliziert, und der Cluster bleibt auch beim
|
||||||
|
Ausfall einzelner Knoten verfügbar. Riak KV steht unter der Apache-2.0-Lizenz.
|
||||||
|
|
||||||
|
\subsection{Nutzwertanalyse}
|
||||||
|
Wir bewerten alle sechs Datenbanken mit den Auswahlkriterien und Gewichten. Jedes Kriterium wird pro Datenbank mit 0 bis 2 Punkten
|
||||||
|
bewertet und mit dem Gewicht multipliziert.
|
||||||
|
|
||||||
|
\begin{table}[h]
|
||||||
|
\centering
|
||||||
|
\small
|
||||||
|
\begin{tabular}{l c c c c c c c}
|
||||||
|
\hline
|
||||||
|
\textbf{Kriterium (Gewicht)} & & \textbf{Redis} & \textbf{Memc.} & \textbf{Arango} & \textbf{Dynamo} & \textbf{etcd} & \textbf{Riak} \\ \hline
|
||||||
|
Hohe Performanz & (5) & 2 & 2 & 1 & 1 & 1 & 1 \\
|
||||||
|
Daten-Streaming & (3) & 2 & 0 & 0 & 2 & 1 & 0 \\
|
||||||
|
Niedrige Latenz & (3) & 2 & 2 & 1 & 1 & 1 & 1 \\
|
||||||
|
Skalierbarkeit & (1) & 1 & 1 & 1 & 2 & 0 & 2 \\
|
||||||
|
Keine Lizenzkosten & (5) & 2 & 2 & 2 & 0 & 2 & 2 \\
|
||||||
|
Indexing & (2) & 1 & 0 & 1 & 1 & 0 & 1 \\
|
||||||
|
Vektorrechnungen & (1) & 1 & 0 & 1 & 0 & 0 & 0 \\
|
||||||
|
Präferenz & (1) & 1 & 0 & 0 & 0 & 0 & 0 \\ \hline
|
||||||
|
\textbf{Gewichtete Summe} & & \textbf{37} & \textbf{27} & \textbf{22} & \textbf{18} & \textbf{21} & \textbf{22} \\
|
||||||
|
\hline
|
||||||
|
\end{tabular}
|
||||||
|
\caption{Nutzwertanalyse der sechs Key-Value-Datenbanken}
|
||||||
|
\label{tab:nutzwertanalyse}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
Redis liegtklar vorne, weil es als einziger Kandidat sowohl bei
|
||||||
|
Performanz und Latenz als auch beim Daten-Streaming die volle Punktzahl erreicht.
|
||||||
|
Memcached kommt bei der reinen Geschwindigkeit zwar mit, scheitert aber am
|
||||||
|
Muss-Kriterium Daten-Streaming. DynamoDB fällt aber wegen der laufenden Kosten am Muss-Kriterium \glqq keine Lizenzkosten\grqq{} durch.
|
||||||
|
ArangoDB ist als multimodale
|
||||||
|
Datenbank flexibel, aber nicht auf unser schreiblastiges Live-Szenario
|
||||||
|
zugeschnitten, etcd ist für Konfigurationsdaten statt Anwendungsdaten gebaut,
|
||||||
|
und bei Riak KV kommt zur fehlenden Streaming-Funktion die kaum noch aktive
|
||||||
|
Entwicklung hinzu.
|
||||||
|
|
||||||
|
\subsection{Datenbankauswahl}
|
||||||
|
Aus den sechs betrachteten Datenbanken wählen wir drei aus, die wir umsetzen
|
||||||
|
und miteinander vergleichen, jeweils eine pro Gruppenmitglied:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Redis} (Justin Rost) --- der Sieger unserer Nutzwertanalyse
|
||||||
|
und unsere Wahl aus der Zwischenpräsentation, bisher aber noch nicht
|
||||||
|
umgesetzt. Redis dient im Vergleich als Referenz, an der sich die beiden
|
||||||
|
anderen Datenbanken messen müssen.
|
||||||
|
\item \textbf{Memcached} (Yasin Ergün) --- der direkteste Konkurrent, da
|
||||||
|
ebenfalls In-Memory und ähnlich schnell. Der Vergleich zeigt, welche
|
||||||
|
Redis-Funktionen wir tatsächlich brauchen und was uns bei einem reinen
|
||||||
|
Cache fehlen würde.
|
||||||
|
\item \textbf{ArangoDB} (Michelle Reichelt) --- die multimodale Datenbank
|
||||||
|
im Feld. Der Vergleich zeigt, was uns eine einzige Datenbank für alle drei
|
||||||
|
Modellierungen aus Kapitel~2 gebracht hätte und was sie an Performanz kostet.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
\end{document}
|
\end{document}
|
||||||
|
|||||||
@ -11,5 +11,15 @@
|
|||||||
\contentsline {subsection}{\numberline {3.1}Vor- und Nachteile des Key-Value-Modells}{8}{subsection.3.1}%
|
\contentsline {subsection}{\numberline {3.1}Vor- und Nachteile des Key-Value-Modells}{8}{subsection.3.1}%
|
||||||
\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}%
|
\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}%
|
||||||
\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}%
|
\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}%
|
||||||
\contentsline {section}{\numberline {4}Fazit}{9}{section.4}%
|
\contentsline {subsection}{\numberline {3.4}Fazit}{9}{subsection.3.4}%
|
||||||
|
\contentsline {section}{\numberline {4}Marktrecherche}{10}{section.4}%
|
||||||
|
\contentsline {subsection}{\numberline {4.1}Betrachtete Datenbanken}{10}{subsection.4.1}%
|
||||||
|
\contentsline {subsubsection}{\numberline {4.1.1}Redis}{10}{subsubsection.4.1.1}%
|
||||||
|
\contentsline {subsubsection}{\numberline {4.1.2}Memcached}{10}{subsubsection.4.1.2}%
|
||||||
|
\contentsline {subsubsection}{\numberline {4.1.3}ArangoDB (multimodal)}{10}{subsubsection.4.1.3}%
|
||||||
|
\contentsline {subsubsection}{\numberline {4.1.4}Amazon DynamoDB}{11}{subsubsection.4.1.4}%
|
||||||
|
\contentsline {subsubsection}{\numberline {4.1.5}etcd}{11}{subsubsection.4.1.5}%
|
||||||
|
\contentsline {subsubsection}{\numberline {4.1.6}Riak KV}{11}{subsubsection.4.1.6}%
|
||||||
|
\contentsline {subsection}{\numberline {4.2}Nutzwertanalyse}{11}{subsection.4.2}%
|
||||||
|
\contentsline {subsection}{\numberline {4.3}Datenbankauswahl}{12}{subsection.4.3}%
|
||||||
\providecommand \tocbasic@end@toc@file {}\tocbasic@end@toc@file
|
\providecommand \tocbasic@end@toc@file {}\tocbasic@end@toc@file
|
||||||
|
|||||||
@ -39,7 +39,9 @@
|
|||||||
\@writefile{toc}{\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}\protected@file@percent }
|
\@writefile{toc}{\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}\protected@file@percent }
|
||||||
\BKM@entry{id=13,dest={73656374696F6E2E34},srcline={340}}{5C3337365C3337375C303030465C303030615C3030307A5C303030695C30303074}
|
\BKM@entry{id=13,dest={73656374696F6E2E34},srcline={340}}{5C3337365C3337375C303030465C303030615C3030307A5C303030695C30303074}
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {4}Fazit}{9}{section.4}\protected@file@percent }
|
\@writefile{toc}{\contentsline {section}{\numberline {4}Fazit}{9}{section.4}\protected@file@percent }
|
||||||
|
\BKM@entry{id=14,dest={73656374696F6E2E35},srcline={368}}{5C3337365C3337375C3030304D5C303030615C303030725C3030306B5C303030745C303030725C303030655C303030635C303030685C303030655C303030725C303030635C303030685C30303065}
|
||||||
|
\@writefile{toc}{\contentsline {section}{\numberline {5}Marktrecherche}{10}{section.5}\protected@file@percent }
|
||||||
\global\@namedef{scr@dte@section@lastmaxnumwidth}{12.14694pt}
|
\global\@namedef{scr@dte@section@lastmaxnumwidth}{12.14694pt}
|
||||||
\global\@namedef{scr@dte@subsection@lastmaxnumwidth}{19.70898pt}
|
\global\@namedef{scr@dte@subsection@lastmaxnumwidth}{19.70898pt}
|
||||||
\@writefile{toc}{\providecommand\tocbasic@end@toc@file{}\tocbasic@end@toc@file}
|
\@writefile{toc}{\providecommand\tocbasic@end@toc@file{}\tocbasic@end@toc@file}
|
||||||
\gdef \@abspage@last{9}
|
\gdef \@abspage@last{10}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Fdb version 4
|
# Fdb version 4
|
||||||
["pdflatex"] 1780687793.04109 "Ringstats_Dokumentation.tex" "build/Ringstats_Dokumentation.pdf" "Ringstats_Dokumentation" 1780687794.00438 0
|
["pdflatex"] 1783429170.25382 "Ringstats_Dokumentation.tex" "build/Ringstats_Dokumentation.pdf" "Ringstats_Dokumentation" 1783429171.46916 0
|
||||||
"/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-t1.enc" 1775415801 2971 def0b6c1f0b107b3b936def894055589 ""
|
"/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-t1.enc" 1775415801 2971 def0b6c1f0b107b3b936def894055589 ""
|
||||||
"/usr/share/texmf-dist/fonts/map/fontname/texfonts.map" 1775415801 3524 cb3e574dea2d1052e39280babc910dc8 ""
|
"/usr/share/texmf-dist/fonts/map/fontname/texfonts.map" 1775415801 3524 cb3e574dea2d1052e39280babc910dc8 ""
|
||||||
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm" 1775415801 3584 402da0b29eafbad07963b1224b222f18 ""
|
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm" 1775415801 3584 402da0b29eafbad07963b1224b222f18 ""
|
||||||
@ -161,10 +161,10 @@
|
|||||||
"/usr/share/texmf-dist/web2c/texmf.cnf" 1775415801 43569 fd570f2fa160877d211e859f687312ba ""
|
"/usr/share/texmf-dist/web2c/texmf.cnf" 1775415801 43569 fd570f2fa160877d211e859f687312ba ""
|
||||||
"/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map" 1780436122 5385779 47c0c393a77fd4a098bacfd57d66b88a ""
|
"/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map" 1780436122 5385779 47c0c393a77fd4a098bacfd57d66b88a ""
|
||||||
"/var/lib/texmf/web2c/pdftex/pdflatex.fmt" 1780436120 3734532 282dcc45f1c728134b92f993ed6db952 ""
|
"/var/lib/texmf/web2c/pdftex/pdflatex.fmt" 1780436120 3734532 282dcc45f1c728134b92f993ed6db952 ""
|
||||||
"Ringstats_Dokumentation.tex" 1780687790.47947 16588 b04ea6d6404d252977bf34dce650e1c0 ""
|
"Ringstats_Dokumentation.tex" 1783429156.65473 16624 26aaf733867a9bf977b13d601b9ee80c ""
|
||||||
"assets/er-modell.png" 1778764597.46144 805989 0b7323ca4b38e6518c24fd9e5684655b ""
|
"assets/er-modell.png" 1778764597.46144 805989 0b7323ca4b38e6518c24fd9e5684655b ""
|
||||||
"build/Ringstats_Dokumentation.aux" 1780687793.96743 8079 e14d3fbb21e9c686a9967800d978e8e7 "pdflatex"
|
"build/Ringstats_Dokumentation.aux" 1783429171.42113 8408 fc30f1f377b4ce8b0a4e3079c56db12c "pdflatex"
|
||||||
"build/Ringstats_Dokumentation.toc" 1780687793.96843 1305 1401ae3dd1fbee665ef3e10a49daf4b1 "pdflatex"
|
"build/Ringstats_Dokumentation.toc" 1783429171.42313 1376 618376327eb7843bc7a20a1d978c49f9 "pdflatex"
|
||||||
(generated)
|
(generated)
|
||||||
"build/Ringstats_Dokumentation.aux"
|
"build/Ringstats_Dokumentation.aux"
|
||||||
"build/Ringstats_Dokumentation.log"
|
"build/Ringstats_Dokumentation.log"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026/Arch Linux) (preloaded format=pdflatex 2026.6.2) 5 JUN 2026 21:29
|
This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026/Arch Linux) (preloaded format=pdflatex 2026.6.2) 7 JUL 2026 14:59
|
||||||
entering extended mode
|
entering extended mode
|
||||||
restricted \write18 enabled.
|
restricted \write18 enabled.
|
||||||
%&-line parsing enabled.
|
%&-line parsing enabled.
|
||||||
@ -722,7 +722,7 @@ LaTeX Font Info: External font `cmex10' loaded for size
|
|||||||
{/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map}{/usr/share/texmf-dist/fonts
|
{/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map}{/usr/share/texmf-dist/fonts
|
||||||
/enc/dvips/cm-super/cm-super-t1.enc}]
|
/enc/dvips/cm-super/cm-super-t1.enc}]
|
||||||
[2]
|
[2]
|
||||||
<assets/er-modell.png, id=48, 3968.8275pt x 4311.10625pt>
|
<assets/er-modell.png, id=50, 3968.8275pt x 4311.10625pt>
|
||||||
File: assets/er-modell.png Graphic file (type png)
|
File: assets/er-modell.png Graphic file (type png)
|
||||||
<use assets/er-modell.png>
|
<use assets/er-modell.png>
|
||||||
Package pdftex.def Info: assets/er-modell.png used on input line 87.
|
Package pdftex.def Info: assets/er-modell.png used on input line 87.
|
||||||
@ -737,17 +737,17 @@ Overfull \hbox (9.63942pt too wide) in paragraph at lines 296--308
|
|||||||
[][]
|
[][]
|
||||||
[]
|
[]
|
||||||
|
|
||||||
[7] [8] [9] (build/Ringstats_Dokumentation.aux)
|
[7] [8] [9] [10] (build/Ringstats_Dokumentation.aux)
|
||||||
***********
|
***********
|
||||||
LaTeX2e <2025-11-01>
|
LaTeX2e <2025-11-01>
|
||||||
L3 programming layer <2026-01-19>
|
L3 programming layer <2026-01-19>
|
||||||
***********
|
***********
|
||||||
)
|
)
|
||||||
Here is how much of TeX's memory you used:
|
Here is how much of TeX's memory you used:
|
||||||
24473 strings out of 469462
|
24476 strings out of 469462
|
||||||
477938 string characters out of 5468615
|
477960 string characters out of 5468615
|
||||||
1009471 words of memory out of 5000000
|
1010471 words of memory out of 5000000
|
||||||
52757 multiletter control sequences out of 15000+600000
|
52758 multiletter control sequences out of 15000+600000
|
||||||
638939 words of font info for 63 fonts, out of 8000000 for 9000
|
638939 words of font info for 63 fonts, out of 8000000 for 9000
|
||||||
14 hyphenation exceptions out of 8191
|
14 hyphenation exceptions out of 8191
|
||||||
108i,8n,108p,11253b,461s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
108i,8n,108p,11253b,461s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||||
@ -758,10 +758,10 @@ m-super/sfsx1200.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx144
|
|||||||
0.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1728.pfb></usr/sha
|
0.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1728.pfb></usr/sha
|
||||||
re/texmf-dist/fonts/type1/public/cm-super/sfsx2488.pfb></usr/share/texmf-dist/f
|
re/texmf-dist/fonts/type1/public/cm-super/sfsx2488.pfb></usr/share/texmf-dist/f
|
||||||
onts/type1/public/cm-super/sftt1200.pfb>
|
onts/type1/public/cm-super/sftt1200.pfb>
|
||||||
Output written on build/Ringstats_Dokumentation.pdf (9 pages, 607609 bytes).
|
Output written on build/Ringstats_Dokumentation.pdf (10 pages, 609620 bytes).
|
||||||
PDF statistics:
|
PDF statistics:
|
||||||
170 PDF objects out of 1000 (max. 8388607)
|
179 PDF objects out of 1000 (max. 8388607)
|
||||||
139 compressed objects within 2 object streams
|
147 compressed objects within 2 object streams
|
||||||
27 named destinations out of 1000 (max. 500000)
|
29 named destinations out of 1000 (max. 500000)
|
||||||
122 words of extra memory for PDF output out of 10000 (max. 10000000)
|
130 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -12,4 +12,5 @@
|
|||||||
\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}%
|
\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}%
|
||||||
\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}%
|
\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}%
|
||||||
\contentsline {section}{\numberline {4}Fazit}{9}{section.4}%
|
\contentsline {section}{\numberline {4}Fazit}{9}{section.4}%
|
||||||
|
\contentsline {section}{\numberline {5}Marktrecherche}{10}{section.5}%
|
||||||
\providecommand \tocbasic@end@toc@file {}\tocbasic@end@toc@file
|
\providecommand \tocbasic@end@toc@file {}\tocbasic@end@toc@file
|
||||||
|
|||||||
48
endpraesi/bench-all.ts
Normal file
48
endpraesi/bench-all.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
const targets = [
|
||||||
|
{ name: "Redis", dir: "client-redis/client-redit" },
|
||||||
|
{ name: "Memcached", dir: "client-memcached" },
|
||||||
|
{ name: "ArangoDB", dir: "client-arangodb" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results: any[] = [];
|
||||||
|
|
||||||
|
for (const t of targets) {
|
||||||
|
console.log(`\n>>> Running benchmark for ${t.name} ...`);
|
||||||
|
const proc = Bun.spawnSync(["bun", "bench.ts"], { cwd: t.dir, stdout: "pipe", stderr: "pipe" });
|
||||||
|
const out = proc.stdout.toString();
|
||||||
|
const err = proc.stderr.toString();
|
||||||
|
process.stdout.write(out.split("\n").filter(l => !l.startsWith("##RESULT##")).join("\n"));
|
||||||
|
if (err.trim()) process.stderr.write(err);
|
||||||
|
const line = out.split("\n").find(l => l.startsWith("##RESULT## "));
|
||||||
|
if (line) results.push(JSON.parse(line.slice("##RESULT## ".length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(s: string, n: number) {
|
||||||
|
return s.padEnd(n);
|
||||||
|
}
|
||||||
|
function padNum(s: string, n: number) {
|
||||||
|
return s.padStart(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testMeta = [
|
||||||
|
{ idx: 0, title: "Anwendungsfall 1 - Schreiben (Live-Player-Sync, 1 Schluessel)" },
|
||||||
|
{ idx: 1, title: "Anwendungsfall 1 - Lesen (Live-Player-Sync, 1 Schluessel)" },
|
||||||
|
{ idx: 2, title: "Anwendungsfall 2 - Lesen (Region / Fast Travel, 14 Schluessel)" },
|
||||||
|
{ idx: 3, title: "Anwendungsfall 2 - Schreiben (Region / Fast Travel, 14 Schluessel)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log("\n\n==================== VERGLEICH ====================");
|
||||||
|
|
||||||
|
for (const m of testMeta) {
|
||||||
|
console.log(`\n${m.title}`);
|
||||||
|
console.log(` ${pad("DB", 12)} | ${padNum("Ops/s", 11)} | ${padNum("ms/Op", 10)} | ${padNum("Gesamt ms", 11)}`);
|
||||||
|
console.log(` ${"-".repeat(12)}-+-${"-".repeat(11)}-+-${"-".repeat(10)}-+-${"-".repeat(11)}`);
|
||||||
|
const rows = results.map(r => ({ db: r.db, t: r.tests[m.idx] }));
|
||||||
|
const fastest = Math.max(...rows.map(x => x.t.opsPerSec));
|
||||||
|
for (const row of rows) {
|
||||||
|
const mark = row.t.opsPerSec === fastest ? " <-- am schnellsten" : "";
|
||||||
|
console.log(` ${pad(row.db, 12)} | ${padNum(Math.round(row.t.opsPerSec).toString(), 11)} | ${padNum(row.t.avgMs.toFixed(4), 10)} | ${padNum(row.t.ms.toFixed(1), 11)}${mark}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n==================================================");
|
||||||
35
endpraesi/client-arangodb/CLAUDE.md
Normal file
35
endpraesi/client-arangodb/CLAUDE.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
Default to using Bun instead of Node.js.
|
||||||
|
|
||||||
|
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||||
|
- Use `bun test` instead of `jest` or `vitest`
|
||||||
|
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||||
|
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||||
|
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||||
|
- Bun automatically loads .env, so don't use dotenv.
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||||
|
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||||
|
- `arangojs` for ArangoDB. ArangoDB has no built-in Bun client.
|
||||||
|
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||||
|
- `WebSocket` is built-in. Don't use `ws`.
|
||||||
|
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||||
|
- Bun.$`ls` instead of execa.
|
||||||
|
|
||||||
|
## Datenmodell (ArangoDB als Key-Value)
|
||||||
|
|
||||||
|
ArangoDB ist multimodal, wir nutzen es hier bewusst als reinen Key-Value-Store,
|
||||||
|
damit der Vergleich direkt zu Redis passt. Alles liegt in einer Collection `kv`.
|
||||||
|
Jedes Dokument hat den Redis-Key als `_key` und den Wert im Feld `value`.
|
||||||
|
|
||||||
|
- `player:{id}` -> value = Objekt
|
||||||
|
- `player:{id}:weapon` / `:armor` / `:talisman` -> value = Array mit item_ids
|
||||||
|
- `item:{id}`, `enemy:{id}`, `region:{id}` -> value = Objekt
|
||||||
|
- `region:{id}:items` / `:enemies`, `enemy:{id}:items` -> value = Array mit ids
|
||||||
|
- `weapon:{id}`, `armor:{id}`, `shield:{id}`, `talisman:{id}`, `item_effect:{id}`, `item_type:{id}` -> value = Objekt
|
||||||
|
|
||||||
|
## Coding Style Preferences
|
||||||
|
|
||||||
|
- Write everything WITHOUT comments.
|
||||||
8
endpraesi/client-arangodb/Doku.md
Normal file
8
endpraesi/client-arangodb/Doku.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Projekt-Dokumentation: Entwicklungsreihenfolge
|
||||||
|
|
||||||
|
1. Projekt init (gleicher Aufbau wie client-redit)
|
||||||
|
2. Docker compose mit arangodb
|
||||||
|
3. Einfache verbindung (arangojs)
|
||||||
|
4. ArangoDB getestet (mit save und document)
|
||||||
|
5. Init-Skript (db/init.ts) mit ~100 statischen Entitäten erstellt (gleiche Daten wie Redis, aber als Key-Value-Dokumente in einer Collection)
|
||||||
|
6. Client (index.ts) aktualisiert Player-Daten live und liest statische Gegner-Daten
|
||||||
33
endpraesi/client-arangodb/README.md
Normal file
33
endpraesi/client-arangodb/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# client-arangodb
|
||||||
|
|
||||||
|
ArangoDB-Variante des Ringstats-Clients. Gleicher Aufbau wie `client-redit`.
|
||||||
|
ArangoDB ist multimodal, wir nutzen es hier bewusst als reinen Key-Value-Store,
|
||||||
|
damit der Vergleich direkt zu Redis passt. Alles liegt in einer Collection `kv`,
|
||||||
|
jedes Dokument hat den Redis-Key als `_key` und den Wert im Feld `value`.
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
Datenbank starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Datenbank befüllen (~100 statische Entitäten):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db/init.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Live-Client starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
ArangoDB läuft auf Port `48529`, damit es parallel zu Redis und Memcached laufen kann.
|
||||||
|
Beim ersten Start braucht ArangoDB kurz, bis es bereit ist. Die Skripte warten automatisch.
|
||||||
92
endpraesi/client-arangodb/bench.ts
Normal file
92
endpraesi/client-arangodb/bench.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Database } from "arangojs";
|
||||||
|
const db = new Database({ url: "http://localhost:48529" });
|
||||||
|
const kv = db.collection("kv");
|
||||||
|
|
||||||
|
const DB_NAME = "ArangoDB";
|
||||||
|
|
||||||
|
const N1 = 2000;
|
||||||
|
const N2 = 300;
|
||||||
|
|
||||||
|
const region1Enemies = ["1", "2", "3", "11", "12", "13", "14"];
|
||||||
|
const region1Items = ["1", "8", "21", "26"];
|
||||||
|
|
||||||
|
const playerObj = (i: number) => ({
|
||||||
|
id: 1,
|
||||||
|
name: "Jonas",
|
||||||
|
health: 50 + (i % 50),
|
||||||
|
stamina: 50 + (i % 50),
|
||||||
|
level: 1 + (i % 20),
|
||||||
|
region_id: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const enemyObj = { id: 0, name: "Benchmark Enemy", health: 1000, level: 20, damage: 50 };
|
||||||
|
const itemObj = { id: 0, name: "Benchmark Item", gear_type: "weapon", gear_id: 1 };
|
||||||
|
const regionObj = { id: 1, name: "Limgrave" };
|
||||||
|
|
||||||
|
async function getVal(key: string): Promise<any> {
|
||||||
|
const doc = await kv.document(key, { graceful: true });
|
||||||
|
return doc ? doc.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setVal(key: string, value: any) {
|
||||||
|
await kv.save({ _key: key, value }, { overwriteMode: "replace" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeSingle(i: number) {
|
||||||
|
await setVal("bench:player:1", playerObj(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSingle(_i: number) {
|
||||||
|
await getVal("bench:player:1");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readRegion(_i: number) {
|
||||||
|
const region = await getVal("region:1");
|
||||||
|
void region?.name;
|
||||||
|
const enemyIds = (await getVal("region:1:enemies")) || [];
|
||||||
|
const itemIds = (await getVal("region:1:items")) || [];
|
||||||
|
for (const id of enemyIds) await getVal(`enemy:${id}`);
|
||||||
|
for (const id of itemIds) await getVal(`item:${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeRegion(_i: number) {
|
||||||
|
await setVal("bench:region:1", regionObj);
|
||||||
|
await setVal("bench:region:1:enemies", region1Enemies);
|
||||||
|
await setVal("bench:region:1:items", region1Items);
|
||||||
|
for (const id of region1Enemies) await setVal(`bench:enemy:${id}`, { ...enemyObj, id });
|
||||||
|
for (const id of region1Items) await setVal(`bench:item:${id}`, { ...itemObj, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function timed(label: string, group: string, iters: number, opsPerIter: number, fn: (i: number) => Promise<void>) {
|
||||||
|
const warmup = Math.min(50, iters);
|
||||||
|
for (let i = 0; i < warmup; i++) await fn(i);
|
||||||
|
const start = performance.now();
|
||||||
|
for (let i = 0; i < iters; i++) await fn(i);
|
||||||
|
const ms = performance.now() - start;
|
||||||
|
const totalOps = iters * opsPerIter;
|
||||||
|
const opsPerSec = totalOps / (ms / 1000);
|
||||||
|
const avgMs = ms / totalOps;
|
||||||
|
return { label, group, iters, opsPerIter, totalOps, ms, opsPerSec, avgMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(r: any) {
|
||||||
|
return ` ${r.label.padEnd(11)} | ${String(r.totalOps).padStart(6)} Ops | ${r.ms.toFixed(1).padStart(9)} ms | ${Math.round(r.opsPerSec).toString().padStart(9)} Ops/s | ${r.avgMs.toFixed(4)} ms/Op`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.version();
|
||||||
|
|
||||||
|
const w1 = await timed("Schreiben", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, writeSingle);
|
||||||
|
const r1 = await timed("Lesen", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, readSingle);
|
||||||
|
const r2 = await timed("Lesen", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, readRegion);
|
||||||
|
const w2 = await timed("Schreiben", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, writeRegion);
|
||||||
|
|
||||||
|
console.log(`\n===== Benchmark: ${DB_NAME} =====`);
|
||||||
|
console.log(w1.group);
|
||||||
|
console.log(fmt(w1));
|
||||||
|
console.log(fmt(r1));
|
||||||
|
console.log(r2.group);
|
||||||
|
console.log(fmt(r2));
|
||||||
|
console.log(fmt(w2));
|
||||||
|
|
||||||
|
console.log("##RESULT## " + JSON.stringify({ db: DB_NAME, tests: [w1, r1, r2, w2] }));
|
||||||
|
process.exit(0);
|
||||||
35
endpraesi/client-arangodb/bun.lock
Normal file
35
endpraesi/client-arangodb/bun.lock
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "client-arangodb",
|
||||||
|
"dependencies": {
|
||||||
|
"arangojs": "^9.2.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
||||||
|
|
||||||
|
"arangojs": ["arangojs@9.3.0", "", { "dependencies": { "@types/node": "^20.11.26" } }, "sha512-+z/TxumH8ywsXAN0oyQAxMnBMtDbd4tjFhGXzygAHD8+YDzEC705STLBh38KGbwxWeekalu2XEHUtrJ/NONgTQ=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
56
endpraesi/client-arangodb/db/init.ts
Normal file
56
endpraesi/client-arangodb/db/init.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Database } from "arangojs";
|
||||||
|
import { effects, types, weapons, armors, shields, talismans, items, regions, enemies } from "../../data";
|
||||||
|
const db = new Database({ url: "http://localhost:48529" });
|
||||||
|
|
||||||
|
console.log("Populating database with ~100 static Ringstats entries...");
|
||||||
|
|
||||||
|
async function waitForArango() {
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
try {
|
||||||
|
await db.version();
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
console.log("Waiting for ArangoDB to be ready...");
|
||||||
|
await Bun.sleep(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("ArangoDB not reachable on localhost:48529");
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForArango();
|
||||||
|
|
||||||
|
const kv = db.collection("kv");
|
||||||
|
if (!(await kv.exists())) {
|
||||||
|
await kv.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs: { _key: string; value: any }[] = [];
|
||||||
|
|
||||||
|
const collect = (prefix: string, list: any[]) => {
|
||||||
|
for (const obj of list) {
|
||||||
|
const { items, enemies, ...data } = obj;
|
||||||
|
docs.push({ _key: `${prefix}:${obj.id}`, value: data });
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
docs.push({ _key: `${prefix}:${obj.id}:items`, value: items });
|
||||||
|
}
|
||||||
|
if (enemies && enemies.length > 0) {
|
||||||
|
docs.push({ _key: `${prefix}:${obj.id}:enemies`, value: enemies });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
collect("item_effect", effects);
|
||||||
|
collect("item_type", types);
|
||||||
|
collect("weapon", weapons);
|
||||||
|
collect("armor", armors);
|
||||||
|
collect("shield", shields);
|
||||||
|
collect("talisman", talismans);
|
||||||
|
collect("item", items);
|
||||||
|
collect("region", regions);
|
||||||
|
collect("enemy", enemies);
|
||||||
|
|
||||||
|
await kv.import(docs, { overwrite: true, onDuplicate: "replace" });
|
||||||
|
|
||||||
|
console.log(`Inserted ${effects.length + types.length + weapons.length + armors.length + shields.length + talismans.length + items.length + regions.length + enemies.length} primary entities into ArangoDB.`);
|
||||||
|
console.log("Database successfully seeded.");
|
||||||
|
process.exit(0);
|
||||||
8
endpraesi/client-arangodb/docker-compose.yml
Normal file
8
endpraesi/client-arangodb/docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
arangodb:
|
||||||
|
image: arangodb:latest
|
||||||
|
environment:
|
||||||
|
ARANGO_NO_AUTH: "1"
|
||||||
|
ports:
|
||||||
|
- "48529:8529"
|
||||||
|
restart: always
|
||||||
153
endpraesi/client-arangodb/index.ts
Normal file
153
endpraesi/client-arangodb/index.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { Database } from "arangojs";
|
||||||
|
const db = new Database({ url: "http://localhost:48529" });
|
||||||
|
const kv = db.collection("kv");
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
magenta: "\x1b[35m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalRequests = 0;
|
||||||
|
|
||||||
|
function log(msg: string, color: string = colors.reset) {
|
||||||
|
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
|
||||||
|
console.log(`${colors.gray}[${timestamp}]${colors.reset} ${color}${msg}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVal(key: string): Promise<any> {
|
||||||
|
const doc = await kv.document(key, { graceful: true });
|
||||||
|
return doc ? doc.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setVal(key: string, value: any) {
|
||||||
|
await kv.save({ _key: key, value }, { overwriteMode: "replace" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const introMessage = "Starting Ringstats Desktop Application...";
|
||||||
|
log(introMessage, colors.cyan);
|
||||||
|
|
||||||
|
const pingStart = performance.now();
|
||||||
|
await db.version();
|
||||||
|
totalRequests++;
|
||||||
|
log(`Connected to Ringstats Database successfully. (Reqs: 1 | Total: ${totalRequests} | Time: ${(performance.now() - pingStart).toFixed(2)}ms)`, colors.green);
|
||||||
|
|
||||||
|
log("Looking for Elden Ring process...", colors.gray);
|
||||||
|
await Bun.sleep(1500);
|
||||||
|
log("Found Elden Ring process!", colors.green);
|
||||||
|
|
||||||
|
log("Equipping initial gear...", colors.gray);
|
||||||
|
await setVal("player:1:weapon", ["1"]);
|
||||||
|
await setVal("player:1:armor", ["11"]);
|
||||||
|
await setVal("player:1:talisman", ["26"]);
|
||||||
|
totalRequests += 3;
|
||||||
|
|
||||||
|
let playerHealth = 100;
|
||||||
|
let playerStamina = 100;
|
||||||
|
let playerLevel = 1;
|
||||||
|
let currentRegionId = 1;
|
||||||
|
|
||||||
|
log("Entering live sync loop (press Ctrl+C to exit)...", colors.gray);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
playerHealth = Math.floor(Math.max(10, playerHealth - (Math.random() * 10) + 5));
|
||||||
|
playerStamina = Math.floor(Math.max(10, playerStamina - (Math.random() * 20) + 10));
|
||||||
|
|
||||||
|
if (Math.random() > 0.9) {
|
||||||
|
playerLevel += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() > 0.85) {
|
||||||
|
currentRegionId = Math.floor(Math.random() * 16) + 1;
|
||||||
|
|
||||||
|
const startTravel = performance.now();
|
||||||
|
const region = await getVal(`region:${currentRegionId}`);
|
||||||
|
const regionEnemiesIds = (await getVal(`region:${currentRegionId}:enemies`)) || [];
|
||||||
|
const regionItemsIds = (await getVal(`region:${currentRegionId}:items`)) || [];
|
||||||
|
|
||||||
|
const enemyNames = await Promise.all(regionEnemiesIds.map(async (id: string) => (await getVal(`enemy:${id}`))?.name));
|
||||||
|
const itemNames = await Promise.all(regionItemsIds.map(async (id: string) => (await getVal(`item:${id}`))?.name));
|
||||||
|
|
||||||
|
const queries = 3 + regionEnemiesIds.length + regionItemsIds.length;
|
||||||
|
totalRequests += queries;
|
||||||
|
const travelTime = (performance.now() - startTravel).toFixed(2);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
log(`>>> Fast Traveled to: [ID: ${currentRegionId}] ${region?.name} <<<`, colors.magenta + colors.bold);
|
||||||
|
log(`[Region Data] Enemies nearby: ${enemyNames.join(", ") || "None"}`, colors.magenta);
|
||||||
|
log(`[Region Data] Items nearby: ${itemNames.join(", ") || "None"}`, colors.magenta);
|
||||||
|
log(`[Stats] Query Time: ${travelTime}ms | Reqs in Block: ${queries} | Total Reqs: ${totalRequests}`, colors.gray);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() > 0.90) {
|
||||||
|
const newWeaponId = Math.floor(Math.random() * 10) + 1;
|
||||||
|
const newArmorId = Math.floor(Math.random() * 10) + 11;
|
||||||
|
const newTalismanId = Math.floor(Math.random() * 5) + 26;
|
||||||
|
|
||||||
|
const startSwap = performance.now();
|
||||||
|
await setVal("player:1:weapon", [newWeaponId.toString()]);
|
||||||
|
await setVal("player:1:armor", [newArmorId.toString()]);
|
||||||
|
await setVal("player:1:talisman", [newTalismanId.toString()]);
|
||||||
|
|
||||||
|
const weaponName = (await getVal(`item:${newWeaponId}`))?.name;
|
||||||
|
const armorName = (await getVal(`item:${newArmorId}`))?.name;
|
||||||
|
const talismanName = (await getVal(`item:${newTalismanId}`))?.name;
|
||||||
|
|
||||||
|
totalRequests += 6;
|
||||||
|
const swapTime = (performance.now() - startSwap).toFixed(2);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
log(`>>> Gear Swapped <<<`, colors.cyan + colors.bold);
|
||||||
|
log(`Equipped: ${weaponName} | ${armorName} | ${talismanName}`, colors.cyan);
|
||||||
|
log(`[Stats] Query Time: ${swapTime}ms | Reqs in Block: 6 | Total Reqs: ${totalRequests}`, colors.gray);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSync = performance.now();
|
||||||
|
await setVal("player:1", {
|
||||||
|
id: 1,
|
||||||
|
name: "Jonas",
|
||||||
|
health: playerHealth,
|
||||||
|
stamina: playerStamina,
|
||||||
|
level: playerLevel,
|
||||||
|
region_id: currentRegionId
|
||||||
|
});
|
||||||
|
totalRequests += 1;
|
||||||
|
const syncTime = (performance.now() - startSync).toFixed(2);
|
||||||
|
|
||||||
|
log(`[Live Sync] [ID: 1] Jonas - Lvl: ${playerLevel} | HP: ${playerHealth} | Stamina: ${playerStamina}`, colors.green);
|
||||||
|
log(`[Stats] Query Time: ${syncTime}ms | Reqs in Block: 1 | Total Reqs: ${totalRequests}`, colors.gray);
|
||||||
|
|
||||||
|
if (Math.random() > 0.7) {
|
||||||
|
const startLock = performance.now();
|
||||||
|
const enemyIds = (await getVal(`region:${currentRegionId}:enemies`)) || [];
|
||||||
|
totalRequests += 1;
|
||||||
|
const randomEnemyId = enemyIds.length > 0 ? enemyIds[Math.floor(Math.random() * enemyIds.length)] : null;
|
||||||
|
|
||||||
|
if (randomEnemyId) {
|
||||||
|
console.log();
|
||||||
|
log("--- Target Lock Detected ---", colors.red + colors.bold);
|
||||||
|
const enemyInfo = await getVal(`enemy:${randomEnemyId}`);
|
||||||
|
|
||||||
|
const dropItemIds = (await getVal(`enemy:${randomEnemyId}:items`)) || [];
|
||||||
|
const dropItemNames = await Promise.all(dropItemIds.map(async (id: string) => (await getVal(`item:${id}`))?.name));
|
||||||
|
|
||||||
|
const queries = 2 + dropItemIds.length;
|
||||||
|
totalRequests += queries;
|
||||||
|
const lockTime = (performance.now() - startLock).toFixed(2);
|
||||||
|
|
||||||
|
log(`Overlay -> [ID: ${enemyInfo.id}] ${enemyInfo.name} | Lvl: ${enemyInfo.level} | HP: ${enemyInfo.health} | DMG: ${enemyInfo.damage}`, colors.yellow);
|
||||||
|
log(`Drops -> ${dropItemNames.length > 0 ? dropItemNames.join(", ") : "None"}`, colors.yellow);
|
||||||
|
log(`[Stats] Query Time: ${lockTime}ms | Reqs in Block: ${queries + 1} | Total Reqs: ${totalRequests}`, colors.gray);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bun.sleep(2000);
|
||||||
|
}
|
||||||
15
endpraesi/client-arangodb/package.json
Normal file
15
endpraesi/client-arangodb/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "client-arangodb",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"arangojs": "^9.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
endpraesi/client-memcached/CLAUDE.md
Normal file
35
endpraesi/client-memcached/CLAUDE.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
Default to using Bun instead of Node.js.
|
||||||
|
|
||||||
|
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||||
|
- Use `bun test` instead of `jest` or `vitest`
|
||||||
|
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||||
|
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||||
|
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||||
|
- Bun automatically loads .env, so don't use dotenv.
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||||
|
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||||
|
- `memjs` for Memcached. Memcached has no built-in Bun client.
|
||||||
|
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||||
|
- `WebSocket` is built-in. Don't use `ws`.
|
||||||
|
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||||
|
- Bun.$`ls` instead of execa.
|
||||||
|
|
||||||
|
## Datenmodell (Memcached)
|
||||||
|
|
||||||
|
Memcached kennt keine Hashes oder Sets, nur reine Key-Value-Paare. Jedes Objekt
|
||||||
|
wird deshalb unter einem flachen Key als JSON-String abgelegt. Listen (z.B. die
|
||||||
|
Items einer Region) landen als JSON-Array im Value unter einem eigenen Key.
|
||||||
|
|
||||||
|
- `player:{id}` -> JSON Objekt
|
||||||
|
- `player:{id}:weapon` / `:armor` / `:talisman` -> JSON Array mit item_ids
|
||||||
|
- `item:{id}`, `enemy:{id}`, `region:{id}` -> JSON Objekt
|
||||||
|
- `region:{id}:items` / `:enemies`, `enemy:{id}:items` -> JSON Array mit ids
|
||||||
|
- `weapon:{id}`, `armor:{id}`, `shield:{id}`, `talisman:{id}`, `item_effect:{id}`, `item_type:{id}` -> JSON Objekt
|
||||||
|
|
||||||
|
## Coding Style Preferences
|
||||||
|
|
||||||
|
- Write everything WITHOUT comments.
|
||||||
8
endpraesi/client-memcached/Doku.md
Normal file
8
endpraesi/client-memcached/Doku.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Projekt-Dokumentation: Entwicklungsreihenfolge
|
||||||
|
|
||||||
|
1. Projekt init (gleicher Aufbau wie client-redit)
|
||||||
|
2. Docker compose mit memcached
|
||||||
|
3. Einfache verbindung (memjs)
|
||||||
|
4. Memcached getestet (mit set und get)
|
||||||
|
5. Init-Skript (db/init.ts) mit ~100 statischen Entitäten erstellt (gleiche Daten wie Redis, aber als flache JSON-Values)
|
||||||
|
6. Client (index.ts) aktualisiert Player-Daten live und liest statische Gegner-Daten
|
||||||
31
endpraesi/client-memcached/README.md
Normal file
31
endpraesi/client-memcached/README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# client-memcached
|
||||||
|
|
||||||
|
Memcached-Variante des Ringstats-Clients. Gleicher Aufbau wie `client-redit`,
|
||||||
|
nur mit Memcached statt Redis. Da Memcached keine Hashes oder Sets kennt, liegt
|
||||||
|
jedes Objekt als flacher Key mit einem JSON-String als Value.
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
Datenbank starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Datenbank befüllen (~100 statische Entitäten):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db/init.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Live-Client starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Memcached läuft auf Port `41211`, damit es parallel zu Redis und ArangoDB laufen kann.
|
||||||
91
endpraesi/client-memcached/bench.ts
Normal file
91
endpraesi/client-memcached/bench.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { Client } from "memjs";
|
||||||
|
const memcached = Client.create("localhost:41211");
|
||||||
|
|
||||||
|
const DB_NAME = "Memcached";
|
||||||
|
|
||||||
|
const N1 = 2000;
|
||||||
|
const N2 = 300;
|
||||||
|
|
||||||
|
const region1Enemies = ["1", "2", "3", "11", "12", "13", "14"];
|
||||||
|
const region1Items = ["1", "8", "21", "26"];
|
||||||
|
|
||||||
|
const playerObj = (i: number) => ({
|
||||||
|
id: 1,
|
||||||
|
name: "Jonas",
|
||||||
|
health: 50 + (i % 50),
|
||||||
|
stamina: 50 + (i % 50),
|
||||||
|
level: 1 + (i % 20),
|
||||||
|
region_id: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const enemyObj = { id: 0, name: "Benchmark Enemy", health: 1000, level: 20, damage: 50 };
|
||||||
|
const itemObj = { id: 0, name: "Benchmark Item", gear_type: "weapon", gear_id: 1 };
|
||||||
|
const regionObj = { id: 1, name: "Limgrave" };
|
||||||
|
|
||||||
|
async function getJson(key: string): Promise<any> {
|
||||||
|
const { value } = await memcached.get(key);
|
||||||
|
return value ? JSON.parse(value.toString()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setJson(key: string, value: any) {
|
||||||
|
await memcached.set(key, JSON.stringify(value), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeSingle(i: number) {
|
||||||
|
await setJson("bench:player:1", playerObj(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSingle(_i: number) {
|
||||||
|
await getJson("bench:player:1");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readRegion(_i: number) {
|
||||||
|
const region = await getJson("region:1");
|
||||||
|
void region?.name;
|
||||||
|
const enemyIds = (await getJson("region:1:enemies")) || [];
|
||||||
|
const itemIds = (await getJson("region:1:items")) || [];
|
||||||
|
for (const id of enemyIds) await getJson(`enemy:${id}`);
|
||||||
|
for (const id of itemIds) await getJson(`item:${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeRegion(_i: number) {
|
||||||
|
await setJson("bench:region:1", regionObj);
|
||||||
|
await setJson("bench:region:1:enemies", region1Enemies);
|
||||||
|
await setJson("bench:region:1:items", region1Items);
|
||||||
|
for (const id of region1Enemies) await setJson(`bench:enemy:${id}`, { ...enemyObj, id });
|
||||||
|
for (const id of region1Items) await setJson(`bench:item:${id}`, { ...itemObj, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function timed(label: string, group: string, iters: number, opsPerIter: number, fn: (i: number) => Promise<void>) {
|
||||||
|
const warmup = Math.min(50, iters);
|
||||||
|
for (let i = 0; i < warmup; i++) await fn(i);
|
||||||
|
const start = performance.now();
|
||||||
|
for (let i = 0; i < iters; i++) await fn(i);
|
||||||
|
const ms = performance.now() - start;
|
||||||
|
const totalOps = iters * opsPerIter;
|
||||||
|
const opsPerSec = totalOps / (ms / 1000);
|
||||||
|
const avgMs = ms / totalOps;
|
||||||
|
return { label, group, iters, opsPerIter, totalOps, ms, opsPerSec, avgMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(r: any) {
|
||||||
|
return ` ${r.label.padEnd(11)} | ${String(r.totalOps).padStart(6)} Ops | ${r.ms.toFixed(1).padStart(9)} ms | ${Math.round(r.opsPerSec).toString().padStart(9)} Ops/s | ${r.avgMs.toFixed(4)} ms/Op`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await memcached.set("__ping__", "1", {});
|
||||||
|
|
||||||
|
const w1 = await timed("Schreiben", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, writeSingle);
|
||||||
|
const r1 = await timed("Lesen", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, readSingle);
|
||||||
|
const r2 = await timed("Lesen", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, readRegion);
|
||||||
|
const w2 = await timed("Schreiben", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, writeRegion);
|
||||||
|
|
||||||
|
console.log(`\n===== Benchmark: ${DB_NAME} =====`);
|
||||||
|
console.log(w1.group);
|
||||||
|
console.log(fmt(w1));
|
||||||
|
console.log(fmt(r1));
|
||||||
|
console.log(r2.group);
|
||||||
|
console.log(fmt(r2));
|
||||||
|
console.log(fmt(w2));
|
||||||
|
|
||||||
|
console.log("##RESULT## " + JSON.stringify({ db: DB_NAME, tests: [w1, r1, r2, w2] }));
|
||||||
|
process.exit(0);
|
||||||
34
endpraesi/client-memcached/bun.lock
Normal file
34
endpraesi/client-memcached/bun.lock
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "client-memcached",
|
||||||
|
"dependencies": {
|
||||||
|
"memjs": "^1.3.2",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/memjs": "^1.3.3",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
|
"@types/memjs": ["@types/memjs@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-OqZuZ2T0ErrygA/WUqQjmsB1GpxAt84OCgKPKbsSWwYKA/oe/lTiqvqMgFOv/Ngl8p+nytgdhrdxJtikOgimCg=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
|
"memjs": ["memjs@1.3.2", "", {}, "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
36
endpraesi/client-memcached/db/init.ts
Normal file
36
endpraesi/client-memcached/db/init.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Client } from "memjs";
|
||||||
|
import { effects, types, weapons, armors, shields, talismans, items, regions, enemies } from "../../data";
|
||||||
|
const memcached = Client.create("localhost:41211");
|
||||||
|
|
||||||
|
console.log("Populating database with ~100 static Ringstats entries...");
|
||||||
|
|
||||||
|
const setJson = async (key: string, value: any) => {
|
||||||
|
await memcached.set(key, JSON.stringify(value), {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setEntities = async (prefix: string, list: any[]) => {
|
||||||
|
for (const obj of list) {
|
||||||
|
const { items, enemies, ...data } = obj;
|
||||||
|
await setJson(`${prefix}:${obj.id}`, data);
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
await setJson(`${prefix}:${obj.id}:items`, items);
|
||||||
|
}
|
||||||
|
if (enemies && enemies.length > 0) {
|
||||||
|
await setJson(`${prefix}:${obj.id}:enemies`, enemies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await setEntities("item_effect", effects);
|
||||||
|
await setEntities("item_type", types);
|
||||||
|
await setEntities("weapon", weapons);
|
||||||
|
await setEntities("armor", armors);
|
||||||
|
await setEntities("shield", shields);
|
||||||
|
await setEntities("talisman", talismans);
|
||||||
|
await setEntities("item", items);
|
||||||
|
await setEntities("region", regions);
|
||||||
|
await setEntities("enemy", enemies);
|
||||||
|
|
||||||
|
console.log(`Inserted ${effects.length + types.length + weapons.length + armors.length + shields.length + talismans.length + items.length + regions.length + enemies.length} primary entities into Memcached.`);
|
||||||
|
console.log("Database successfully seeded.");
|
||||||
|
process.exit(0);
|
||||||
6
endpraesi/client-memcached/docker-compose.yml
Normal file
6
endpraesi/client-memcached/docker-compose.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
memcached:
|
||||||
|
image: memcached:alpine
|
||||||
|
ports:
|
||||||
|
- "41211:11211"
|
||||||
|
restart: always
|
||||||
153
endpraesi/client-memcached/index.ts
Normal file
153
endpraesi/client-memcached/index.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { Client } from "memjs";
|
||||||
|
const memcached = Client.create("localhost:41211");
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
magenta: "\x1b[35m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalRequests = 0;
|
||||||
|
|
||||||
|
function log(msg: string, color: string = colors.reset) {
|
||||||
|
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
|
||||||
|
console.log(`${colors.gray}[${timestamp}]${colors.reset} ${color}${msg}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJson(key: string): Promise<any> {
|
||||||
|
const { value } = await memcached.get(key);
|
||||||
|
return value ? JSON.parse(value.toString()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setJson(key: string, value: any) {
|
||||||
|
await memcached.set(key, JSON.stringify(value), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const introMessage = "Starting Ringstats Desktop Application...";
|
||||||
|
log(introMessage, colors.cyan);
|
||||||
|
|
||||||
|
const pingStart = performance.now();
|
||||||
|
await memcached.set("__ping__", "1", {});
|
||||||
|
await memcached.get("__ping__");
|
||||||
|
totalRequests++;
|
||||||
|
log(`Connected to Ringstats Database successfully. (Reqs: 1 | Total: ${totalRequests} | Time: ${(performance.now() - pingStart).toFixed(2)}ms)`, colors.green);
|
||||||
|
|
||||||
|
log("Looking for Elden Ring process...", colors.gray);
|
||||||
|
await Bun.sleep(1500);
|
||||||
|
log("Found Elden Ring process!", colors.green);
|
||||||
|
|
||||||
|
log("Equipping initial gear...", colors.gray);
|
||||||
|
await setJson("player:1:weapon", ["1"]);
|
||||||
|
await setJson("player:1:armor", ["11"]);
|
||||||
|
await setJson("player:1:talisman", ["26"]);
|
||||||
|
totalRequests += 3;
|
||||||
|
|
||||||
|
let playerHealth = 100;
|
||||||
|
let playerStamina = 100;
|
||||||
|
let playerLevel = 1;
|
||||||
|
let currentRegionId = 1;
|
||||||
|
|
||||||
|
log("Entering live sync loop (press Ctrl+C to exit)...", colors.gray);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
playerHealth = Math.floor(Math.max(10, playerHealth - (Math.random() * 10) + 5));
|
||||||
|
playerStamina = Math.floor(Math.max(10, playerStamina - (Math.random() * 20) + 10));
|
||||||
|
|
||||||
|
if (Math.random() > 0.9) {
|
||||||
|
playerLevel += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() > 0.85) {
|
||||||
|
currentRegionId = Math.floor(Math.random() * 16) + 1;
|
||||||
|
|
||||||
|
const startTravel = performance.now();
|
||||||
|
const region = await getJson(`region:${currentRegionId}`);
|
||||||
|
const regionEnemiesIds = (await getJson(`region:${currentRegionId}:enemies`)) || [];
|
||||||
|
const regionItemsIds = (await getJson(`region:${currentRegionId}:items`)) || [];
|
||||||
|
|
||||||
|
const enemyNames = await Promise.all(regionEnemiesIds.map(async (id: string) => (await getJson(`enemy:${id}`))?.name));
|
||||||
|
const itemNames = await Promise.all(regionItemsIds.map(async (id: string) => (await getJson(`item:${id}`))?.name));
|
||||||
|
|
||||||
|
const queries = 3 + regionEnemiesIds.length + regionItemsIds.length;
|
||||||
|
totalRequests += queries;
|
||||||
|
const travelTime = (performance.now() - startTravel).toFixed(2);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
log(`>>> Fast Traveled to: [ID: ${currentRegionId}] ${region?.name} <<<`, colors.magenta + colors.bold);
|
||||||
|
log(`[Region Data] Enemies nearby: ${enemyNames.join(", ") || "None"}`, colors.magenta);
|
||||||
|
log(`[Region Data] Items nearby: ${itemNames.join(", ") || "None"}`, colors.magenta);
|
||||||
|
log(`[Stats] Query Time: ${travelTime}ms | Reqs in Block: ${queries} | Total Reqs: ${totalRequests}`, colors.gray);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() > 0.90) {
|
||||||
|
const newWeaponId = Math.floor(Math.random() * 10) + 1;
|
||||||
|
const newArmorId = Math.floor(Math.random() * 10) + 11;
|
||||||
|
const newTalismanId = Math.floor(Math.random() * 5) + 26;
|
||||||
|
|
||||||
|
const startSwap = performance.now();
|
||||||
|
await setJson("player:1:weapon", [newWeaponId.toString()]);
|
||||||
|
await setJson("player:1:armor", [newArmorId.toString()]);
|
||||||
|
await setJson("player:1:talisman", [newTalismanId.toString()]);
|
||||||
|
|
||||||
|
const weaponName = (await getJson(`item:${newWeaponId}`))?.name;
|
||||||
|
const armorName = (await getJson(`item:${newArmorId}`))?.name;
|
||||||
|
const talismanName = (await getJson(`item:${newTalismanId}`))?.name;
|
||||||
|
|
||||||
|
totalRequests += 6;
|
||||||
|
const swapTime = (performance.now() - startSwap).toFixed(2);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
log(`>>> Gear Swapped <<<`, colors.cyan + colors.bold);
|
||||||
|
log(`Equipped: ${weaponName} | ${armorName} | ${talismanName}`, colors.cyan);
|
||||||
|
log(`[Stats] Query Time: ${swapTime}ms | Reqs in Block: 6 | Total Reqs: ${totalRequests}`, colors.gray);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSync = performance.now();
|
||||||
|
await setJson("player:1", {
|
||||||
|
id: 1,
|
||||||
|
name: "Jonas",
|
||||||
|
health: playerHealth,
|
||||||
|
stamina: playerStamina,
|
||||||
|
level: playerLevel,
|
||||||
|
region_id: currentRegionId
|
||||||
|
});
|
||||||
|
totalRequests += 1;
|
||||||
|
const syncTime = (performance.now() - startSync).toFixed(2);
|
||||||
|
|
||||||
|
log(`[Live Sync] [ID: 1] Jonas - Lvl: ${playerLevel} | HP: ${playerHealth} | Stamina: ${playerStamina}`, colors.green);
|
||||||
|
log(`[Stats] Query Time: ${syncTime}ms | Reqs in Block: 1 | Total Reqs: ${totalRequests}`, colors.gray);
|
||||||
|
|
||||||
|
if (Math.random() > 0.7) {
|
||||||
|
const startLock = performance.now();
|
||||||
|
const enemyIds = (await getJson(`region:${currentRegionId}:enemies`)) || [];
|
||||||
|
totalRequests += 1;
|
||||||
|
const randomEnemyId = enemyIds.length > 0 ? enemyIds[Math.floor(Math.random() * enemyIds.length)] : null;
|
||||||
|
|
||||||
|
if (randomEnemyId) {
|
||||||
|
console.log();
|
||||||
|
log("--- Target Lock Detected ---", colors.red + colors.bold);
|
||||||
|
const enemyInfo = await getJson(`enemy:${randomEnemyId}`);
|
||||||
|
|
||||||
|
const dropItemIds = (await getJson(`enemy:${randomEnemyId}:items`)) || [];
|
||||||
|
const dropItemNames = await Promise.all(dropItemIds.map(async (id: string) => (await getJson(`item:${id}`))?.name));
|
||||||
|
|
||||||
|
const queries = 2 + dropItemIds.length;
|
||||||
|
totalRequests += queries;
|
||||||
|
const lockTime = (performance.now() - startLock).toFixed(2);
|
||||||
|
|
||||||
|
log(`Overlay -> [ID: ${enemyInfo.id}] ${enemyInfo.name} | Lvl: ${enemyInfo.level} | HP: ${enemyInfo.health} | DMG: ${enemyInfo.damage}`, colors.yellow);
|
||||||
|
log(`Drops -> ${dropItemNames.length > 0 ? dropItemNames.join(", ") : "None"}`, colors.yellow);
|
||||||
|
log(`[Stats] Query Time: ${lockTime}ms | Reqs in Block: ${queries + 1} | Total Reqs: ${totalRequests}`, colors.gray);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bun.sleep(2000);
|
||||||
|
}
|
||||||
16
endpraesi/client-memcached/package.json
Normal file
16
endpraesi/client-memcached/package.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "client-memcached",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"memjs": "^1.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/memjs": "^1.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
endpraesi/client-memcached/tsconfig.json
Normal file
29
endpraesi/client-memcached/tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
81
endpraesi/client-redis/bench.ts
Normal file
81
endpraesi/client-redis/bench.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { RedisClient } from "bun";
|
||||||
|
const redis = new RedisClient("redis://localhost:40379");
|
||||||
|
|
||||||
|
const DB_NAME = "Redis";
|
||||||
|
|
||||||
|
const N1 = 2000;
|
||||||
|
const N2 = 300;
|
||||||
|
|
||||||
|
const region1Enemies = ["1", "2", "3", "11", "12", "13", "14"];
|
||||||
|
const region1Items = ["1", "8", "21", "26"];
|
||||||
|
|
||||||
|
const playerObj = (i: number) => ({
|
||||||
|
id: 1,
|
||||||
|
name: "Jonas",
|
||||||
|
health: 50 + (i % 50),
|
||||||
|
stamina: 50 + (i % 50),
|
||||||
|
level: 1 + (i % 20),
|
||||||
|
region_id: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const enemyObj = { id: 0, name: "Benchmark Enemy", health: 1000, level: 20, damage: 50 };
|
||||||
|
const itemObj = { id: 0, name: "Benchmark Item", gear_type: "weapon", gear_id: 1 };
|
||||||
|
const regionObj = { id: 1, name: "Limgrave" };
|
||||||
|
|
||||||
|
async function writeSingle(i: number) {
|
||||||
|
await redis.hset("bench:player:1", playerObj(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSingle(_i: number) {
|
||||||
|
await redis.hgetall("bench:player:1");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readRegion(_i: number) {
|
||||||
|
await redis.hget("region:1", "name");
|
||||||
|
const enemyIds = await redis.smembers("region:1:enemies");
|
||||||
|
const itemIds = await redis.smembers("region:1:items");
|
||||||
|
for (const id of enemyIds) await redis.hgetall(`enemy:${id}`);
|
||||||
|
for (const id of itemIds) await redis.hgetall(`item:${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeRegion(_i: number) {
|
||||||
|
await redis.hset("bench:region:1", regionObj);
|
||||||
|
await redis.sadd("bench:region:1:enemies", ...region1Enemies);
|
||||||
|
await redis.sadd("bench:region:1:items", ...region1Items);
|
||||||
|
for (const id of region1Enemies) await redis.hset(`bench:enemy:${id}`, { ...enemyObj, id });
|
||||||
|
for (const id of region1Items) await redis.hset(`bench:item:${id}`, { ...itemObj, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function timed(label: string, group: string, iters: number, opsPerIter: number, fn: (i: number) => Promise<void>) {
|
||||||
|
const warmup = Math.min(50, iters);
|
||||||
|
for (let i = 0; i < warmup; i++) await fn(i);
|
||||||
|
const start = performance.now();
|
||||||
|
for (let i = 0; i < iters; i++) await fn(i);
|
||||||
|
const ms = performance.now() - start;
|
||||||
|
const totalOps = iters * opsPerIter;
|
||||||
|
const opsPerSec = totalOps / (ms / 1000);
|
||||||
|
const avgMs = ms / totalOps;
|
||||||
|
return { label, group, iters, opsPerIter, totalOps, ms, opsPerSec, avgMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(r: any) {
|
||||||
|
return ` ${r.label.padEnd(11)} | ${String(r.totalOps).padStart(6)} Ops | ${r.ms.toFixed(1).padStart(9)} ms | ${Math.round(r.opsPerSec).toString().padStart(9)} Ops/s | ${r.avgMs.toFixed(4)} ms/Op`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.ping();
|
||||||
|
|
||||||
|
const w1 = await timed("Schreiben", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, writeSingle);
|
||||||
|
const r1 = await timed("Lesen", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, readSingle);
|
||||||
|
const r2 = await timed("Lesen", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, readRegion);
|
||||||
|
const w2 = await timed("Schreiben", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, writeRegion);
|
||||||
|
|
||||||
|
console.log(`\n===== Benchmark: ${DB_NAME} =====`);
|
||||||
|
console.log(w1.group);
|
||||||
|
console.log(fmt(w1));
|
||||||
|
console.log(fmt(r1));
|
||||||
|
console.log(r2.group);
|
||||||
|
console.log(fmt(r2));
|
||||||
|
console.log(fmt(w2));
|
||||||
|
|
||||||
|
console.log("##RESULT## " + JSON.stringify({ db: DB_NAME, tests: [w1, r1, r2, w2] }));
|
||||||
|
process.exit(0);
|
||||||
5846
endpraesi/client-redis/db/fanapi_data.json
Normal file
5846
endpraesi/client-redis/db/fanapi_data.json
Normal file
File diff suppressed because it is too large
Load Diff
125
endpraesi/client-redis/db/fetchData.ts
Normal file
125
endpraesi/client-redis/db/fetchData.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { write } from "bun";
|
||||||
|
|
||||||
|
const API_BASE = "https://eldenring.fanapis.com/api";
|
||||||
|
|
||||||
|
async function fetchAll(endpoint: string) {
|
||||||
|
let allData: any[] = [];
|
||||||
|
let page = 0;
|
||||||
|
const limit = 100;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const res = await fetch(`${API_BASE}/${endpoint}?limit=${limit}&page=${page}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error(`Error fetching ${endpoint}:`, res.status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.data || data.data.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
allData = allData.concat(data.data);
|
||||||
|
console.log(`Fetched ${allData.length} ${endpoint}...`);
|
||||||
|
if (data.data.length < limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
return allData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRegion(entityRegion: string | null | undefined, locationStr: string | null | undefined, locationLookup: Map<string, string>, uniqueRegions: Set<string>): string | null {
|
||||||
|
// 1. If the entity has a region, use it (and clean up typos from API)
|
||||||
|
if (entityRegion) {
|
||||||
|
let r = entityRegion;
|
||||||
|
if (r === "Liunia of the Lakes") r = "Liurnia of the Lakes";
|
||||||
|
if (r === "Mountaintop of the Giants") r = "Mountaintops of the Giants";
|
||||||
|
if (r === "Altus Pleateau") r = "Altus Plateau";
|
||||||
|
uniqueRegions.add(r);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Lookup via locations map
|
||||||
|
if (locationStr) {
|
||||||
|
for (const [locName, regionName] of locationLookup.entries()) {
|
||||||
|
if (locationStr.toLowerCase().includes(locName.toLowerCase())) {
|
||||||
|
return regionName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback: check if the location string itself contains a known dynamic region
|
||||||
|
for (const region of uniqueRegions) {
|
||||||
|
if (locationStr.toLowerCase().includes(region.toLowerCase())) {
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Fetching locations...");
|
||||||
|
const locations = await fetchAll("locations");
|
||||||
|
const uniqueRegions = new Set<string>();
|
||||||
|
const locationLookup = new Map<string, string>();
|
||||||
|
|
||||||
|
// Normalize regions and build a lookup map: LocationName -> Region
|
||||||
|
for (const loc of locations) {
|
||||||
|
if (loc.region) {
|
||||||
|
let r = loc.region;
|
||||||
|
if (r === "Liunia of the Lakes" || r === "Liurnia") r = "Liurnia of the Lakes";
|
||||||
|
if (r === "Mountaintop of the Giants") r = "Mountaintops of the Giants";
|
||||||
|
if (r === "Altus Pleateau") r = "Altus Plateau";
|
||||||
|
uniqueRegions.add(r);
|
||||||
|
locationLookup.set(loc.name, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Fetching bosses...");
|
||||||
|
const bosses = await fetchAll("bosses");
|
||||||
|
|
||||||
|
console.log("Fetching creatures...");
|
||||||
|
const creatures = await fetchAll("creatures");
|
||||||
|
|
||||||
|
console.log("Fetching npcs...");
|
||||||
|
const npcs = await fetchAll("npcs");
|
||||||
|
|
||||||
|
const livingEntities = [
|
||||||
|
...bosses.map(b => ({ ...b, _type: "boss" })),
|
||||||
|
...creatures.map(c => ({ ...c, _type: "creature" })),
|
||||||
|
...npcs.map(n => ({ ...n, _type: "npc" }))
|
||||||
|
];
|
||||||
|
|
||||||
|
const cleanedEntities = livingEntities.map(entity => {
|
||||||
|
// combine all possible location info
|
||||||
|
const cleanRegion = extractRegion(entity.region, entity.location, locationLookup, uniqueRegions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entity.id,
|
||||||
|
name: entity.name,
|
||||||
|
type: entity._type,
|
||||||
|
cleanRegion: cleanRegion || "Unknown",
|
||||||
|
originalLocation: entity.location,
|
||||||
|
drops: entity.drops || []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save regions based on what was dynamically found in the API
|
||||||
|
const regionsData = Array.from(uniqueRegions).map((r, i) => ({ id: i + 1, name: r }));
|
||||||
|
|
||||||
|
console.log("Fetching items...");
|
||||||
|
const items = await fetchAll("items");
|
||||||
|
// you might also want weapons, armors etc depending on what "items" means to the user.
|
||||||
|
// The fan API separates items, weapons, armors, etc. Let's just fetch items for now.
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
regions: regionsData,
|
||||||
|
entities: cleanedEntities,
|
||||||
|
items: items.map(i => ({ id: i.id, name: i.name, type: i.type, effect: i.effect }))
|
||||||
|
};
|
||||||
|
|
||||||
|
await write("./db/fanapi_data.json", JSON.stringify(output, null, 2));
|
||||||
|
console.log("Saved to db/fanapi_data.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
34
endpraesi/client-redis/db/init.ts
Normal file
34
endpraesi/client-redis/db/init.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { RedisClient } from "bun";
|
||||||
|
import { effects, types, weapons, armors, shields, talismans, items, regions, enemies } from "../../data";
|
||||||
|
const redis = new RedisClient("redis://localhost:40379");
|
||||||
|
|
||||||
|
console.log("Populating database with ~100 static Ringstats entries...");
|
||||||
|
|
||||||
|
// Helper for hset
|
||||||
|
const setHash = async (prefix: string, list: any[]) => {
|
||||||
|
for (const obj of list) {
|
||||||
|
// extract items/enemies sets if they exist
|
||||||
|
const { items, enemies, ...hashData } = obj;
|
||||||
|
await redis.hset(`${prefix}:${obj.id}`, hashData);
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
await redis.sadd(`${prefix}:${obj.id}:items`, ...items);
|
||||||
|
}
|
||||||
|
if (enemies && enemies.length > 0) {
|
||||||
|
await redis.sadd(`${prefix}:${obj.id}:enemies`, ...enemies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await setHash("item_effect", effects);
|
||||||
|
await setHash("item_type", types);
|
||||||
|
await setHash("weapon", weapons);
|
||||||
|
await setHash("armor", armors);
|
||||||
|
await setHash("shield", shields);
|
||||||
|
await setHash("talisman", talismans);
|
||||||
|
await setHash("item", items);
|
||||||
|
await setHash("region", regions);
|
||||||
|
await setHash("enemy", enemies);
|
||||||
|
|
||||||
|
console.log(`Inserted ${effects.length + types.length + weapons.length + armors.length + shields.length + talismans.length + items.length + regions.length + enemies.length} primary entities into Redis.`);
|
||||||
|
console.log("Database successfully seeded.");
|
||||||
|
process.exit(0);
|
||||||
29
endpraesi/client-redis/tsconfig.json
Normal file
29
endpraesi/client-redis/tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,8 @@
|
|||||||
import { RedisClient } from "bun";
|
/**
|
||||||
const redis = new RedisClient("redis://localhost:40379");
|
* KI GENERIERT
|
||||||
|
*/
|
||||||
|
|
||||||
console.log("Populating database with ~100 static Ringstats entries...");
|
export const effects = [
|
||||||
|
|
||||||
const effects = [
|
|
||||||
{ id: 1, name: "Stärke", value: 5 },
|
{ id: 1, name: "Stärke", value: 5 },
|
||||||
{ id: 2, name: "Geschick", value: 5 },
|
{ id: 2, name: "Geschick", value: 5 },
|
||||||
{ id: 3, name: "Blutung", value: 50 },
|
{ id: 3, name: "Blutung", value: 50 },
|
||||||
@ -16,7 +15,7 @@ const effects = [
|
|||||||
{ id: 10, name: "Blitzschaden", value: 25 },
|
{ id: 10, name: "Blitzschaden", value: 25 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const types = [
|
export const types = [
|
||||||
{ id: 1, item_id: 101, name: "Dolch" },
|
{ id: 1, item_id: 101, name: "Dolch" },
|
||||||
{ id: 2, item_id: 102, name: "Gerades Schwert" },
|
{ id: 2, item_id: 102, name: "Gerades Schwert" },
|
||||||
{ id: 3, item_id: 103, name: "Großschwert" },
|
{ id: 3, item_id: 103, name: "Großschwert" },
|
||||||
@ -29,20 +28,20 @@ const types = [
|
|||||||
{ id: 10, item_id: 110, name: "Talisman-Amulett" },
|
{ id: 10, item_id: 110, name: "Talisman-Amulett" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const weapons = [
|
export const weapons = [
|
||||||
{ id: 1, item_effect_id: 3, item_type_id: 4, weight: 5.5, damage: 115 }, // Uchigatana
|
{ id: 1, item_effect_id: 3, item_type_id: 4, weight: 5.5, damage: 115 },
|
||||||
{ id: 2, item_effect_id: 3, item_type_id: 4, weight: 6.5, damage: 118 }, // Rivers of Blood
|
{ id: 2, item_effect_id: 3, item_type_id: 4, weight: 6.5, damage: 118 },
|
||||||
{ id: 3, item_effect_id: 8, item_type_id: 3, weight: 10.0, damage: 160 }, // Dark Moon Greatsword
|
{ id: 3, item_effect_id: 8, item_type_id: 3, weight: 10.0, damage: 160 },
|
||||||
{ id: 4, item_effect_id: 1, item_type_id: 3, weight: 23.0, damage: 210 }, // Guts Greatsword
|
{ id: 4, item_effect_id: 1, item_type_id: 3, weight: 23.0, damage: 210 },
|
||||||
{ id: 5, item_effect_id: 3, item_type_id: 1, weight: 1.5, damage: 74 }, // Reduvia
|
{ id: 5, item_effect_id: 3, item_type_id: 1, weight: 1.5, damage: 74 },
|
||||||
{ id: 6, item_effect_id: 9, item_type_id: 2, weight: 4.0, damage: 105 }, // Blasphemous Blade
|
{ id: 6, item_effect_id: 9, item_type_id: 2, weight: 4.0, damage: 105 },
|
||||||
{ id: 7, item_effect_id: 10, item_type_id: 4, weight: 6.0, damage: 110 }, // Dragonscale Blade
|
{ id: 7, item_effect_id: 10, item_type_id: 4, weight: 6.0, damage: 110 },
|
||||||
{ id: 8, item_effect_id: 1, item_type_id: 3, weight: 15.0, damage: 175 }, // Claymore
|
{ id: 8, item_effect_id: 1, item_type_id: 3, weight: 15.0, damage: 175 },
|
||||||
{ id: 9, item_effect_id: 2, item_type_id: 1, weight: 2.0, damage: 80 }, // Misericorde
|
{ id: 9, item_effect_id: 2, item_type_id: 1, weight: 2.0, damage: 80 },
|
||||||
{ id: 10, item_effect_id: 6, item_type_id: 2, weight: 4.5, damage: 100 }, // Icerind Hatchet (simulated as straight sword for simplicity)
|
{ id: 10, item_effect_id: 6, item_type_id: 2, weight: 4.5, damage: 100 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const armors = [
|
export const armors = [
|
||||||
{ id: 1, item_effect_id: 1, item_type_id: 5, weight: 4.6, defense: 14 },
|
{ id: 1, item_effect_id: 1, item_type_id: 5, weight: 4.6, defense: 14 },
|
||||||
{ id: 2, item_effect_id: 1, item_type_id: 6, weight: 10.6, defense: 35 },
|
{ id: 2, item_effect_id: 1, item_type_id: 6, weight: 10.6, defense: 35 },
|
||||||
{ id: 3, item_effect_id: 1, item_type_id: 7, weight: 3.5, defense: 11 },
|
{ id: 3, item_effect_id: 1, item_type_id: 7, weight: 3.5, defense: 11 },
|
||||||
@ -55,7 +54,7 @@ const armors = [
|
|||||||
{ id: 10, item_effect_id: 10, item_type_id: 6, weight: 7.0, defense: 22 },
|
{ id: 10, item_effect_id: 10, item_type_id: 6, weight: 7.0, defense: 22 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const shields = [
|
export const shields = [
|
||||||
{ id: 1, item_effect_id: 1, item_type_id: 9, weight: 3.5, damage: 45, defense: 100 },
|
{ id: 1, item_effect_id: 1, item_type_id: 9, weight: 3.5, damage: 45, defense: 100 },
|
||||||
{ id: 2, item_effect_id: 8, item_type_id: 9, weight: 4.5, damage: 55, defense: 100 },
|
{ id: 2, item_effect_id: 8, item_type_id: 9, weight: 4.5, damage: 55, defense: 100 },
|
||||||
{ id: 3, item_effect_id: 9, item_type_id: 9, weight: 5.5, damage: 65, defense: 100 },
|
{ id: 3, item_effect_id: 9, item_type_id: 9, weight: 5.5, damage: 65, defense: 100 },
|
||||||
@ -63,7 +62,7 @@ const shields = [
|
|||||||
{ id: 5, item_effect_id: 6, item_type_id: 9, weight: 3.0, damage: 40, defense: 95 },
|
{ id: 5, item_effect_id: 6, item_type_id: 9, weight: 3.0, damage: 40, defense: 95 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const talismans = [
|
export const talismans = [
|
||||||
{ id: 1, item_effect_id: 1 },
|
{ id: 1, item_effect_id: 1 },
|
||||||
{ id: 2, item_effect_id: 2 },
|
{ id: 2, item_effect_id: 2 },
|
||||||
{ id: 3, item_effect_id: 7 },
|
{ id: 3, item_effect_id: 7 },
|
||||||
@ -71,8 +70,7 @@ const talismans = [
|
|||||||
{ id: 5, item_effect_id: 9 },
|
{ id: 5, item_effect_id: 9 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const items = [
|
export const items = [
|
||||||
// Weapons (id 1-10)
|
|
||||||
{ id: 1, name: "Uchigatana", gear_type: "weapon", gear_id: 1 },
|
{ id: 1, name: "Uchigatana", gear_type: "weapon", gear_id: 1 },
|
||||||
{ id: 2, name: "Rivers of Blood", gear_type: "weapon", gear_id: 2 },
|
{ id: 2, name: "Rivers of Blood", gear_type: "weapon", gear_id: 2 },
|
||||||
{ id: 3, name: "Dark Moon Greatsword", gear_type: "weapon", gear_id: 3 },
|
{ id: 3, name: "Dark Moon Greatsword", gear_type: "weapon", gear_id: 3 },
|
||||||
@ -83,7 +81,6 @@ const items = [
|
|||||||
{ id: 8, name: "Claymore", gear_type: "weapon", gear_id: 8 },
|
{ id: 8, name: "Claymore", gear_type: "weapon", gear_id: 8 },
|
||||||
{ id: 9, name: "Misericorde", gear_type: "weapon", gear_id: 9 },
|
{ id: 9, name: "Misericorde", gear_type: "weapon", gear_id: 9 },
|
||||||
{ id: 10, name: "Icerind Hatchet", gear_type: "weapon", gear_id: 10 },
|
{ id: 10, name: "Icerind Hatchet", gear_type: "weapon", gear_id: 10 },
|
||||||
// Armors (id 11-20)
|
|
||||||
{ id: 11, name: "Knight Helm", gear_type: "armor", gear_id: 1 },
|
{ id: 11, name: "Knight Helm", gear_type: "armor", gear_id: 1 },
|
||||||
{ id: 12, name: "Knight Armor", gear_type: "armor", gear_id: 2 },
|
{ id: 12, name: "Knight Armor", gear_type: "armor", gear_id: 2 },
|
||||||
{ id: 13, name: "Knight Gauntlets", gear_type: "armor", gear_id: 3 },
|
{ id: 13, name: "Knight Gauntlets", gear_type: "armor", gear_id: 3 },
|
||||||
@ -94,13 +91,11 @@ const items = [
|
|||||||
{ id: 18, name: "Radahn Lion Armor", gear_type: "armor", gear_id: 8 },
|
{ id: 18, name: "Radahn Lion Armor", gear_type: "armor", gear_id: 8 },
|
||||||
{ id: 19, name: "Carian Knight Helm", gear_type: "armor", gear_id: 9 },
|
{ id: 19, name: "Carian Knight Helm", gear_type: "armor", gear_id: 9 },
|
||||||
{ id: 20, name: "Carian Knight Armor", gear_type: "armor", gear_id: 10 },
|
{ id: 20, name: "Carian Knight Armor", gear_type: "armor", gear_id: 10 },
|
||||||
// Shields (id 21-25)
|
|
||||||
{ id: 21, name: "Brass Shield", gear_type: "shield", gear_id: 1 },
|
{ id: 21, name: "Brass Shield", gear_type: "shield", gear_id: 1 },
|
||||||
{ id: 22, name: "Carian Knight Shield", gear_type: "shield", gear_id: 2 },
|
{ id: 22, name: "Carian Knight Shield", gear_type: "shield", gear_id: 2 },
|
||||||
{ id: 23, name: "Kite Shield", gear_type: "shield", gear_id: 3 },
|
{ id: 23, name: "Kite Shield", gear_type: "shield", gear_id: 3 },
|
||||||
{ id: 24, name: "Banished Knight Shield", gear_type: "shield", gear_id: 4 },
|
{ id: 24, name: "Banished Knight Shield", gear_type: "shield", gear_id: 4 },
|
||||||
{ id: 25, name: "Heater Shield", gear_type: "shield", gear_id: 5 },
|
{ id: 25, name: "Heater Shield", gear_type: "shield", gear_id: 5 },
|
||||||
// Talismans (id 26-30)
|
|
||||||
{ id: 26, name: "Erdtree's Favor", gear_type: "talisman", gear_id: 1 },
|
{ id: 26, name: "Erdtree's Favor", gear_type: "talisman", gear_id: 1 },
|
||||||
{ id: 27, name: "Radagon's Soreseal", gear_type: "talisman", gear_id: 2 },
|
{ id: 27, name: "Radagon's Soreseal", gear_type: "talisman", gear_id: 2 },
|
||||||
{ id: 28, name: "Crimson Amber Medallion", gear_type: "talisman", gear_id: 3 },
|
{ id: 28, name: "Crimson Amber Medallion", gear_type: "talisman", gear_id: 3 },
|
||||||
@ -128,7 +123,7 @@ const items = [
|
|||||||
{ id: 50, name: "Ghost Glovewort [9]", gear_type: "material" },
|
{ id: 50, name: "Ghost Glovewort [9]", gear_type: "material" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const regions = [
|
export const regions = [
|
||||||
{ id: 1, name: "Limgrave", items: ["1", "8", "21", "26"], enemies: ["1", "2", "3", "11", "12", "13", "14"] },
|
{ id: 1, name: "Limgrave", items: ["1", "8", "21", "26"], enemies: ["1", "2", "3", "11", "12", "13", "14"] },
|
||||||
{ id: 2, name: "Liurnia of the Lakes", items: ["3", "15", "16", "19", "20", "22", "29"], enemies: ["4", "5", "15", "16", "17", "18"] },
|
{ id: 2, name: "Liurnia of the Lakes", items: ["3", "15", "16", "19", "20", "22", "29"], enemies: ["4", "5", "15", "16", "17", "18"] },
|
||||||
{ id: 3, name: "Caelid", items: ["4", "17", "18"], enemies: ["6", "7", "19", "20", "21", "22"] },
|
{ id: 3, name: "Caelid", items: ["4", "17", "18"], enemies: ["6", "7", "19", "20", "21", "22"] },
|
||||||
@ -147,7 +142,7 @@ const regions = [
|
|||||||
{ id: 16, name: "Leyndell, Ashen Capital", items: [], enemies: [] },
|
{ id: 16, name: "Leyndell, Ashen Capital", items: [], enemies: [] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const enemies = [
|
export const enemies = [
|
||||||
{ id: 1, region_id: 1, name: "Tree Sentinel", health: 3000, level: 20, damage: 70, items: ["1", "47", "34"] },
|
{ id: 1, region_id: 1, name: "Tree Sentinel", health: 3000, level: 20, damage: 70, items: ["1", "47", "34"] },
|
||||||
{ id: 2, region_id: 1, name: "Margit, the Fell Omen", health: 4100, level: 25, damage: 100, items: ["26", "33", "34"] },
|
{ id: 2, region_id: 1, name: "Margit, the Fell Omen", health: 4100, level: 25, damage: 100, items: ["26", "33", "34"] },
|
||||||
{ id: 3, region_id: 1, name: "Godrick the Grafted", health: 6000, level: 30, damage: 150, items: ["33", "39"] },
|
{ id: 3, region_id: 1, name: "Godrick the Grafted", health: 6000, level: 30, damage: 150, items: ["33", "39"] },
|
||||||
@ -179,32 +174,3 @@ const enemies = [
|
|||||||
{ id: 29, region_id: 6, name: "Beastman of Farum Azula", health: 1000, level: 80, damage: 200, items: ["37", "48"] },
|
{ id: 29, region_id: 6, name: "Beastman of Farum Azula", health: 1000, level: 80, damage: 200, items: ["37", "48"] },
|
||||||
{ id: 30, region_id: 6, name: "Banished Knight", health: 2000, level: 85, damage: 280, items: ["24", "37", "48", "50"] },
|
{ id: 30, region_id: 6, name: "Banished Knight", health: 2000, level: 85, damage: 280, items: ["24", "37", "48", "50"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Helper for hset
|
|
||||||
const setHash = async (prefix: string, list: any[]) => {
|
|
||||||
for (const obj of list) {
|
|
||||||
// extract items/enemies sets if they exist
|
|
||||||
const { items, enemies, ...hashData } = obj;
|
|
||||||
await redis.hset(`${prefix}:${obj.id}`, hashData);
|
|
||||||
if (items && items.length > 0) {
|
|
||||||
await redis.sadd(`${prefix}:${obj.id}:items`, ...items);
|
|
||||||
}
|
|
||||||
if (enemies && enemies.length > 0) {
|
|
||||||
await redis.sadd(`${prefix}:${obj.id}:enemies`, ...enemies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await setHash("item_effect", effects);
|
|
||||||
await setHash("item_type", types);
|
|
||||||
await setHash("weapon", weapons);
|
|
||||||
await setHash("armor", armors);
|
|
||||||
await setHash("shield", shields);
|
|
||||||
await setHash("talisman", talismans);
|
|
||||||
await setHash("item", items);
|
|
||||||
await setHash("region", regions);
|
|
||||||
await setHash("enemy", enemies);
|
|
||||||
|
|
||||||
console.log(`Inserted ${effects.length + types.length + weapons.length + armors.length + shields.length + talismans.length + items.length + regions.length + enemies.length} primary entities into Redis.`);
|
|
||||||
console.log("Database successfully seeded.");
|
|
||||||
process.exit(0);
|
|
||||||
Binary file not shown.
@ -1,118 +0,0 @@
|
|||||||
import { serve, RedisClient } from "bun";
|
|
||||||
import index from "./index.html";
|
|
||||||
|
|
||||||
const redis = new RedisClient("redis://localhost:40379");
|
|
||||||
|
|
||||||
const server = serve({
|
|
||||||
routes: {
|
|
||||||
"/*": index,
|
|
||||||
|
|
||||||
"/api/player": async (req) => {
|
|
||||||
try {
|
|
||||||
const player = await redis.hgetall("player:1");
|
|
||||||
|
|
||||||
if (!player || !player.id) {
|
|
||||||
return Response.json({ error: "Player not found" }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentRegionId = player.region_id;
|
|
||||||
let region = null;
|
|
||||||
|
|
||||||
if (currentRegionId) {
|
|
||||||
const regionName = await redis.hget(`region:${currentRegionId}`, "name");
|
|
||||||
const enemiesIds = await redis.smembers(`region:${currentRegionId}:enemies`);
|
|
||||||
const itemsIds = await redis.smembers(`region:${currentRegionId}:items`);
|
|
||||||
|
|
||||||
const enemies = [];
|
|
||||||
for (const id of enemiesIds) {
|
|
||||||
const enemy = await redis.hgetall(`enemy:${id}`);
|
|
||||||
if (enemy && enemy.id) enemies.push(enemy);
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = [];
|
|
||||||
for (const id of itemsIds) {
|
|
||||||
const item = await redis.hgetall(`item:${id}`);
|
|
||||||
if (item && item.id) items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
region = {
|
|
||||||
id: currentRegionId,
|
|
||||||
name: regionName,
|
|
||||||
enemies,
|
|
||||||
items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ player, region });
|
|
||||||
} catch (e: any) {
|
|
||||||
return Response.json({ error: e.message }, { status: 500 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"/api/universe": async (req) => {
|
|
||||||
try {
|
|
||||||
// Fetch all regions 1-16
|
|
||||||
const regions = [];
|
|
||||||
for (let i = 1; i <= 16; i++) {
|
|
||||||
const name = await redis.hget(`region:${i}`, "name");
|
|
||||||
if (name) {
|
|
||||||
const enemiesIds = await redis.smembers(`region:${i}:enemies`);
|
|
||||||
const itemsIds = await redis.smembers(`region:${i}:items`);
|
|
||||||
regions.push({ id: i.toString(), name, enemiesIds, itemsIds });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch enemies 1-30
|
|
||||||
const enemies = [];
|
|
||||||
for (let i = 1; i <= 30; i++) {
|
|
||||||
const enemy = await redis.hgetall(`enemy:${i}`);
|
|
||||||
if (enemy && enemy.id) {
|
|
||||||
const itemsIds = await redis.smembers(`enemy:${i}:items`);
|
|
||||||
enemy.itemsIds = itemsIds || [];
|
|
||||||
enemies.push(enemy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch items 1-50
|
|
||||||
const items = [];
|
|
||||||
for (let i = 1; i <= 50; i++) {
|
|
||||||
const item = await redis.hgetall(`item:${i}`);
|
|
||||||
if (item && item.id) {
|
|
||||||
if (item.gear_type && item.gear_id) {
|
|
||||||
const gearStats = await redis.hgetall(`${item.gear_type}:${item.gear_id}`);
|
|
||||||
|
|
||||||
if (gearStats.item_type_id) {
|
|
||||||
const typeInfo = await redis.hgetall(`item_type:${gearStats.item_type_id}`);
|
|
||||||
if (typeInfo && typeInfo.name) gearStats.Item_Type = typeInfo.name;
|
|
||||||
delete gearStats.item_type_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gearStats.item_effect_id) {
|
|
||||||
const effectInfo = await redis.hgetall(`item_effect:${gearStats.item_effect_id}`);
|
|
||||||
if (effectInfo && effectInfo.name) {
|
|
||||||
gearStats.Item_Effect = `${effectInfo.name} ${effectInfo.value ? `(+${effectInfo.value})` : ""}`;
|
|
||||||
}
|
|
||||||
delete gearStats.item_effect_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete gearStats.id; // redundant
|
|
||||||
|
|
||||||
item.stats = gearStats;
|
|
||||||
}
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ regions, enemies, items });
|
|
||||||
} catch (e: any) {
|
|
||||||
return Response.json({ error: e.message }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
development: process.env.NODE_ENV !== "production" && {
|
|
||||||
hmr: true,
|
|
||||||
console: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`🚀 Server running at ${server.url}`);
|
|
||||||
34
endpraesi/website/.gitignore
vendored
Normal file
34
endpraesi/website/.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
@ -5,10 +5,12 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "bun-react-template",
|
"name": "bun-react-template",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"arangojs": "^9.2.0",
|
||||||
"bun-plugin-tailwind": "^0.1.2",
|
"bun-plugin-tailwind": "^0.1.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.42.2",
|
"framer-motion": "^12.42.2",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
|
"memjs": "^1.3.2",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
@ -16,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"@types/memjs": "^1.3.3",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
},
|
},
|
||||||
@ -56,12 +59,16 @@
|
|||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
|
"@types/memjs": ["@types/memjs@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-OqZuZ2T0ErrygA/WUqQjmsB1GpxAt84OCgKPKbsSWwYKA/oe/lTiqvqMgFOv/Ngl8p+nytgdhrdxJtikOgimCg=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="],
|
"@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"arangojs": ["arangojs@9.3.0", "", { "dependencies": { "@types/node": "^20.11.26" } }, "sha512-+z/TxumH8ywsXAN0oyQAxMnBMtDbd4tjFhGXzygAHD8+YDzEC705STLBh38KGbwxWeekalu2XEHUtrJ/NONgTQ=="],
|
||||||
|
|
||||||
"bun": ["bun@1.3.14", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.14", "@oven/bun-darwin-x64": "1.3.14", "@oven/bun-darwin-x64-baseline": "1.3.14", "@oven/bun-freebsd-aarch64": "1.3.14", "@oven/bun-freebsd-x64": "1.3.14", "@oven/bun-linux-aarch64": "1.3.14", "@oven/bun-linux-aarch64-android": "1.3.14", "@oven/bun-linux-aarch64-musl": "1.3.14", "@oven/bun-linux-x64": "1.3.14", "@oven/bun-linux-x64-android": "1.3.14", "@oven/bun-linux-x64-baseline": "1.3.14", "@oven/bun-linux-x64-musl": "1.3.14", "@oven/bun-linux-x64-musl-baseline": "1.3.14", "@oven/bun-windows-aarch64": "1.3.14", "@oven/bun-windows-x64": "1.3.14", "@oven/bun-windows-x64-baseline": "1.3.14" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg=="],
|
"bun": ["bun@1.3.14", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.14", "@oven/bun-darwin-x64": "1.3.14", "@oven/bun-darwin-x64-baseline": "1.3.14", "@oven/bun-freebsd-aarch64": "1.3.14", "@oven/bun-freebsd-x64": "1.3.14", "@oven/bun-linux-aarch64": "1.3.14", "@oven/bun-linux-aarch64-android": "1.3.14", "@oven/bun-linux-aarch64-musl": "1.3.14", "@oven/bun-linux-x64": "1.3.14", "@oven/bun-linux-x64-android": "1.3.14", "@oven/bun-linux-x64-baseline": "1.3.14", "@oven/bun-linux-x64-musl": "1.3.14", "@oven/bun-linux-x64-musl-baseline": "1.3.14", "@oven/bun-windows-aarch64": "1.3.14", "@oven/bun-windows-x64": "1.3.14", "@oven/bun-windows-x64-baseline": "1.3.14" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg=="],
|
||||||
|
|
||||||
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
||||||
@ -76,6 +83,8 @@
|
|||||||
|
|
||||||
"lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="],
|
"lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="],
|
||||||
|
|
||||||
|
"memjs": ["memjs@1.3.2", "", {}, "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg=="],
|
||||||
|
|
||||||
"motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="],
|
"motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="],
|
||||||
|
|
||||||
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||||
@ -93,5 +102,9 @@
|
|||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
|
"arangojs/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
||||||
|
|
||||||
|
"arangojs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,10 +9,12 @@
|
|||||||
"build": "bun run build.ts"
|
"build": "bun run build.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"arangojs": "^9.2.0",
|
||||||
"bun-plugin-tailwind": "^0.1.2",
|
"bun-plugin-tailwind": "^0.1.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.42.2",
|
"framer-motion": "^12.42.2",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
|
"memjs": "^1.3.2",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
@ -21,6 +23,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest",
|
||||||
|
"@types/memjs": "^1.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,7 +3,7 @@ import "./index.css";
|
|||||||
import {
|
import {
|
||||||
Database, Home, Search, Search as SearchIcon, Map as MapIcon, Shield, Sword,
|
Database, Home, Search, Search as SearchIcon, Map as MapIcon, Shield, Sword,
|
||||||
Heart, Activity, User, Box, ChevronDown, ChevronRight,
|
Heart, Activity, User, Box, ChevronDown, ChevronRight,
|
||||||
Zap, Crosshair, Droplets
|
Zap, Crosshair, Droplets, Clock
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
@ -13,10 +13,23 @@ function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Ms({ ms, className }: { ms?: number, className?: string }) {
|
||||||
|
if (ms == null) return null;
|
||||||
|
return (
|
||||||
|
<span className={cn("inline-flex items-center gap-1 font-mono text-[10px] text-indigo-300/80 tabular-nums bg-indigo-500/10 border border-indigo-500/20 rounded px-1.5 py-0.5", className)}>
|
||||||
|
<Clock className="w-2.5 h-2.5" />
|
||||||
|
{ms.toFixed(3)} ms
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type Player = { id: string, name: string, health: string, stamina: string, level: string, region_id: string };
|
type Player = { id: string, name: string, health: string, stamina: string, level: string, region_id: string };
|
||||||
type Enemy = { id: string, name: string, health: string, level: string, damage: string, itemsIds: string[], region_id: string };
|
type Enemy = { id: string, name: string, health: string, level: string, damage: string, itemsIds: string[], region_id: string, _ms?: number };
|
||||||
type Item = { id: string, name: string, gear_type?: string, gear_id?: string, stats?: Record<string, string> };
|
type Item = { id: string, name: string, gear_type?: string, gear_id?: string, stats?: Record<string, string>, _ms?: number };
|
||||||
type Region = { id: string, name: string, enemiesIds: string[], itemsIds: string[], enemies?: Enemy[], items?: Item[] };
|
type Region = { id: string, name: string, enemiesIds: string[], itemsIds: string[], enemies?: Enemy[], items?: Item[], _ms?: number };
|
||||||
|
|
||||||
|
type Timing = { op: string, key: string, ms: number };
|
||||||
|
type Timings = { count: number, total: number, ops: Timing[] };
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [activeTab, setActiveTab] = useState<"home" | "search">("home");
|
const [activeTab, setActiveTab] = useState<"home" | "search">("home");
|
||||||
@ -25,6 +38,8 @@ export function App() {
|
|||||||
|
|
||||||
const [playerData, setPlayerData] = useState<{player: Player, region: Region} | null>(null);
|
const [playerData, setPlayerData] = useState<{player: Player, region: Region} | null>(null);
|
||||||
const [universe, setUniverse] = useState<{regions: Region[], enemies: Enemy[], items: Item[]} | null>(null);
|
const [universe, setUniverse] = useState<{regions: Region[], enemies: Enemy[], items: Item[]} | null>(null);
|
||||||
|
const [playerTimings, setPlayerTimings] = useState<Timings | null>(null);
|
||||||
|
const [universeTimings, setUniverseTimings] = useState<Timings | null>(null);
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
||||||
@ -32,20 +47,27 @@ export function App() {
|
|||||||
const [selectedEnemy, setSelectedEnemy] = useState<Enemy | null>(null);
|
const [selectedEnemy, setSelectedEnemy] = useState<Enemy | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/universe")
|
setUniverse(null);
|
||||||
|
setUniverseTimings(null);
|
||||||
|
fetch(`/api/universe?db=${db}`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => setUniverse(data))
|
.then(data => {
|
||||||
|
setUniverse(data);
|
||||||
|
setUniverseTimings(data._timings || null);
|
||||||
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}, []);
|
}, [db]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (db !== "redis") return;
|
setPlayerData(null);
|
||||||
|
setPlayerTimings(null);
|
||||||
const fetchPlayer = async () => {
|
const fetchPlayer = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/player");
|
const res = await fetch(`/api/player?db=${db}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setPlayerData(data);
|
setPlayerData(data);
|
||||||
|
setPlayerTimings(data._timings || null);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -68,10 +90,12 @@ export function App() {
|
|||||||
|
|
||||||
const dbOptions = [
|
const dbOptions = [
|
||||||
{ id: "redis", name: "Redis" },
|
{ id: "redis", name: "Redis" },
|
||||||
{ id: "mongodb", name: "MongoDB" },
|
{ id: "memcached", name: "Memcached" },
|
||||||
{ id: "neo4j", name: "Neo4j" }
|
{ id: "arangodb", name: "ArangoDB" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const dbLabel = dbOptions.find(o => o.id === db)?.name || db;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-slate-950 text-slate-200 overflow-hidden font-sans selection:bg-indigo-500/30">
|
<div className="flex h-screen bg-slate-950 text-slate-200 overflow-hidden font-sans selection:bg-indigo-500/30">
|
||||||
|
|
||||||
@ -157,7 +181,7 @@ export function App() {
|
|||||||
db === option.id ? "bg-indigo-500/10 text-indigo-300" : "hover:bg-white/5 text-slate-300"
|
db === option.id ? "bg-indigo-500/10 text-indigo-300" : "hover:bg-white/5 text-slate-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{option.name} {option.id !== "redis" && "(WIP)"}
|
{option.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -173,21 +197,13 @@ export function App() {
|
|||||||
<div className="absolute top-0 left-1/4 w-96 h-96 bg-indigo-500/10 rounded-full blur-[120px] pointer-events-none" />
|
<div className="absolute top-0 left-1/4 w-96 h-96 bg-indigo-500/10 rounded-full blur-[120px] pointer-events-none" />
|
||||||
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-[120px] pointer-events-none" />
|
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-[120px] pointer-events-none" />
|
||||||
|
|
||||||
{db !== "redis" ? (
|
|
||||||
<div className="h-full flex flex-col items-center justify-center text-slate-500">
|
|
||||||
<Database className="w-16 h-16 mb-4 opacity-20" />
|
|
||||||
<h2 className="text-xl font-medium">Database not implemented yet.</h2>
|
|
||||||
<p>Please select Redis to view live data.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{activeTab === "home" ? (
|
{activeTab === "home" ? (
|
||||||
<HomeView key="home" data={playerData} onItemClick={setSelectedItem} onRegionClick={setSelectedRegion} onEnemyClick={setSelectedEnemy} />
|
<HomeView key="home" data={playerData} timings={playerTimings} dbLabel={dbLabel} onItemClick={setSelectedItem} onRegionClick={setSelectedRegion} />
|
||||||
) : (
|
) : (
|
||||||
<SearchView key="search" universe={universe} onItemClick={setSelectedItem} onRegionClick={setSelectedRegion} onEnemyClick={setSelectedEnemy} />
|
<SearchView key="search" universe={universe} timings={universeTimings} dbLabel={dbLabel} onItemClick={setSelectedItem} onRegionClick={setSelectedRegion} />
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -205,6 +221,7 @@ export function App() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Box className="w-5 h-5 text-indigo-400" />
|
<Box className="w-5 h-5 text-indigo-400" />
|
||||||
<h3 className="text-lg font-bold text-white">{selectedItem.name}</h3>
|
<h3 className="text-lg font-bold text-white">{selectedItem.name}</h3>
|
||||||
|
<Ms ms={selectedItem._ms} />
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setSelectedItem(null)} className="text-slate-400 hover:text-white transition-colors">
|
<button onClick={() => setSelectedItem(null)} className="text-slate-400 hover:text-white transition-colors">
|
||||||
✕
|
✕
|
||||||
@ -290,6 +307,7 @@ export function App() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Sword className="w-5 h-5 text-rose-400" />
|
<Sword className="w-5 h-5 text-rose-400" />
|
||||||
<h3 className="text-lg font-bold text-white">{selectedEnemy.name}</h3>
|
<h3 className="text-lg font-bold text-white">{selectedEnemy.name}</h3>
|
||||||
|
<Ms ms={selectedEnemy._ms} />
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setSelectedEnemy(null)} className="text-slate-400 hover:text-white transition-colors">
|
<button onClick={() => setSelectedEnemy(null)} className="text-slate-400 hover:text-white transition-colors">
|
||||||
✕
|
✕
|
||||||
@ -382,6 +400,7 @@ export function App() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<MapIcon className="w-5 h-5 text-purple-400" />
|
<MapIcon className="w-5 h-5 text-purple-400" />
|
||||||
<h3 className="text-lg font-bold text-white">{selectedRegion.name}</h3>
|
<h3 className="text-lg font-bold text-white">{selectedRegion.name}</h3>
|
||||||
|
<Ms ms={selectedRegion._ms} />
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setSelectedRegion(null)} className="text-slate-400 hover:text-white transition-colors">
|
<button onClick={() => setSelectedRegion(null)} className="text-slate-400 hover:text-white transition-colors">
|
||||||
✕
|
✕
|
||||||
@ -445,14 +464,70 @@ export function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeView({ data, onItemClick, onRegionClick }: { data: any, onItemClick: (item: Item) => void, onRegionClick: (r: Region) => void }) {
|
function TimingPanel({ title, timings, regionCached }: { title: string, timings: Timings | null, regionCached?: boolean }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
if (!timings) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-900/60 border border-white/5 rounded-2xl overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
className="w-full flex items-center justify-between p-4 hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20">
|
||||||
|
<Clock className="w-4 h-4 text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-slate-300">{title}</span>
|
||||||
|
{regionCached && (
|
||||||
|
<span className="font-mono text-[10px] text-emerald-300/90 tabular-nums bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-0.5">
|
||||||
|
Region aus Cache
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-xs text-slate-500 tabular-nums">{timings.count} Ops</span>
|
||||||
|
<span className="text-sm font-mono font-bold text-indigo-300 tabular-nums">{timings.total.toFixed(2)} ms</span>
|
||||||
|
<motion.div animate={{ rotate: open ? 180 : 0 }}>
|
||||||
|
<ChevronDown className="w-4 h-4 text-slate-500" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
className="border-t border-white/5 bg-black/20"
|
||||||
|
>
|
||||||
|
<div className="max-h-72 overflow-y-auto divide-y divide-white/5">
|
||||||
|
{timings.ops.map((op, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between px-4 py-2 text-xs gap-4">
|
||||||
|
<span className="font-mono text-slate-400 truncate">
|
||||||
|
<span className="text-indigo-300/70">{op.op}</span> <span className="text-slate-500">{op.key}</span>
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-slate-300 tabular-nums shrink-0">{op.ms.toFixed(3)} ms</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HomeView({ data, timings, dbLabel, onItemClick, onRegionClick }: { data: any, timings: Timings | null, dbLabel: string, onItemClick: (item: Item) => void, onRegionClick: (r: Region) => void }) {
|
||||||
if (!data) return (
|
if (!data) return (
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-500"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-500"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const { player, region } = data;
|
const { player, region, regionCached } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -461,6 +536,8 @@ function HomeView({ data, onItemClick, onRegionClick }: { data: any, onItemClick
|
|||||||
exit={{ opacity: 0, y: -20 }}
|
exit={{ opacity: 0, y: -20 }}
|
||||||
className="max-w-5xl mx-auto space-y-8"
|
className="max-w-5xl mx-auto space-y-8"
|
||||||
>
|
>
|
||||||
|
<TimingPanel title={`${dbLabel} · Live Sync (Player + Region)`} timings={timings} regionCached={regionCached} />
|
||||||
|
|
||||||
{/* Player Stats Card */}
|
{/* Player Stats Card */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="col-span-1 md:col-span-2 bg-gradient-to-br from-slate-900 to-slate-800/80 p-8 rounded-3xl border border-white/10 relative overflow-hidden group">
|
<div className="col-span-1 md:col-span-2 bg-gradient-to-br from-slate-900 to-slate-800/80 p-8 rounded-3xl border border-white/10 relative overflow-hidden group">
|
||||||
@ -515,6 +592,7 @@ function HomeView({ data, onItemClick, onRegionClick }: { data: any, onItemClick
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm text-slate-400 font-medium tracking-widest uppercase mb-2">Current Location</h3>
|
<h3 className="text-sm text-slate-400 font-medium tracking-widest uppercase mb-2">Current Location</h3>
|
||||||
<div className="text-2xl font-bold text-white group-hover:text-purple-100 transition-colors">{region?.name || "Unknown"}</div>
|
<div className="text-2xl font-bold text-white group-hover:text-purple-100 transition-colors">{region?.name || "Unknown"}</div>
|
||||||
|
{region?._ms != null && <div className="mt-2 flex justify-center"><Ms ms={region._ms} /></div>}
|
||||||
<div className="mt-3 text-xs text-purple-400 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1">
|
<div className="mt-3 text-xs text-purple-400 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1">
|
||||||
View Details <ChevronRight className="w-3 h-3" />
|
View Details <ChevronRight className="w-3 h-3" />
|
||||||
</div>
|
</div>
|
||||||
@ -576,6 +654,7 @@ function HomeView({ data, onItemClick, onRegionClick }: { data: any, onItemClick
|
|||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-white truncate max-w-[120px]">{item.name}</div>
|
<div className="font-medium text-white truncate max-w-[120px]">{item.name}</div>
|
||||||
<div className="text-xs text-slate-500 capitalize">{item.gear_type || "Item"}</div>
|
<div className="text-xs text-slate-500 capitalize">{item.gear_type || "Item"}</div>
|
||||||
|
<div className="mt-1"><Ms ms={item._ms} /></div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@ -607,6 +686,7 @@ function EnemyCard({ enemy, onItemClick, allItems, allRegions }: { enemy: Enemy,
|
|||||||
<div className="font-bold text-white text-lg">{enemy.name}</div>
|
<div className="font-bold text-white text-lg">{enemy.name}</div>
|
||||||
<div className="flex items-center gap-3 mt-1">
|
<div className="flex items-center gap-3 mt-1">
|
||||||
<span className="text-xs font-medium px-2 py-0.5 rounded bg-slate-800 text-slate-300">Lvl {enemy.level}</span>
|
<span className="text-xs font-medium px-2 py-0.5 rounded bg-slate-800 text-slate-300">Lvl {enemy.level}</span>
|
||||||
|
<Ms ms={enemy._ms} />
|
||||||
{regionName && (
|
{regionName && (
|
||||||
<span className="text-xs font-medium text-slate-400 flex items-center gap-1 truncate max-w-[150px]">
|
<span className="text-xs font-medium text-slate-400 flex items-center gap-1 truncate max-w-[150px]">
|
||||||
<MapIcon className="w-3 h-3 text-purple-400" />
|
<MapIcon className="w-3 h-3 text-purple-400" />
|
||||||
@ -675,7 +755,7 @@ function EnemyCard({ enemy, onItemClick, allItems, allRegions }: { enemy: Enemy,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchView({ universe, onItemClick, onRegionClick }: { universe: any, onItemClick: (item: Item) => void, onRegionClick: (r: Region) => void }) {
|
function SearchView({ universe, timings, dbLabel, onItemClick, onRegionClick }: { universe: any, timings: Timings | null, dbLabel: string, onItemClick: (item: Item) => void, onRegionClick: (r: Region) => void }) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
if (!universe) return (
|
if (!universe) return (
|
||||||
@ -697,6 +777,8 @@ function SearchView({ universe, onItemClick, onRegionClick }: { universe: any, o
|
|||||||
exit={{ opacity: 0, y: -20 }}
|
exit={{ opacity: 0, y: -20 }}
|
||||||
className="max-w-5xl mx-auto space-y-8 h-full flex flex-col"
|
className="max-w-5xl mx-auto space-y-8 h-full flex flex-col"
|
||||||
>
|
>
|
||||||
|
<TimingPanel title={`${dbLabel} · Universe geladen`} timings={timings} />
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<SearchIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
<SearchIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||||
<input
|
<input
|
||||||
244
endpraesi/website/src/index.ts
Normal file
244
endpraesi/website/src/index.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import { serve, RedisClient } from "bun";
|
||||||
|
import { Client } from "memjs";
|
||||||
|
import { Database } from "arangojs";
|
||||||
|
import index from "./index.html";
|
||||||
|
|
||||||
|
const redis = new RedisClient("redis://localhost:40379");
|
||||||
|
const memcached = Client.create("localhost:41211");
|
||||||
|
const arango = new Database({ url: "http://localhost:48529" });
|
||||||
|
const arangoKv = arango.collection("kv");
|
||||||
|
|
||||||
|
type Timing = { op: string; key: string; ms: number };
|
||||||
|
|
||||||
|
type Adapter = {
|
||||||
|
timings: Timing[];
|
||||||
|
getObject: (key: string) => Promise<any>;
|
||||||
|
getField: (key: string, field: string) => Promise<any>;
|
||||||
|
getIds: (key: string) => Promise<string[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function stringifyObj(o: any) {
|
||||||
|
if (!o) return null;
|
||||||
|
const out: any = {};
|
||||||
|
for (const k in o) out[k] = o[k] == null ? o[k] : String(o[k]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redisAdapter(): Adapter {
|
||||||
|
const timings: Timing[] = [];
|
||||||
|
const track = async <T>(op: string, key: string, fn: () => Promise<T>): Promise<T> => {
|
||||||
|
const start = performance.now();
|
||||||
|
const res = await fn();
|
||||||
|
timings.push({ op, key, ms: performance.now() - start });
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
timings,
|
||||||
|
getObject: (key) =>
|
||||||
|
track("hgetall", key, async () => {
|
||||||
|
const o = await redis.hgetall(key);
|
||||||
|
return o && Object.keys(o).length ? o : null;
|
||||||
|
}),
|
||||||
|
getField: (key, field) => track("hget", key, () => redis.hget(key, field)),
|
||||||
|
getIds: (key) => track("smembers", key, async () => (await redis.smembers(key)) || []),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function memcachedAdapter(): Adapter {
|
||||||
|
const timings: Timing[] = [];
|
||||||
|
const getJson = async (key: string) => {
|
||||||
|
const { value } = await memcached.get(key);
|
||||||
|
return value ? JSON.parse(value.toString()) : null;
|
||||||
|
};
|
||||||
|
const track = async <T>(op: string, key: string, fn: () => Promise<T>): Promise<T> => {
|
||||||
|
const start = performance.now();
|
||||||
|
const res = await fn();
|
||||||
|
timings.push({ op, key, ms: performance.now() - start });
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
timings,
|
||||||
|
getObject: (key) => track("get", key, async () => stringifyObj(await getJson(key))),
|
||||||
|
getField: (key, field) => track("get", key, async () => (await getJson(key))?.[field] ?? null),
|
||||||
|
getIds: (key) => track("get", key, async () => ((await getJson(key)) || []).map(String)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function arangoAdapter(): Adapter {
|
||||||
|
const timings: Timing[] = [];
|
||||||
|
const getVal = async (key: string) => {
|
||||||
|
const doc = await arangoKv.document(key, { graceful: true });
|
||||||
|
return doc ? doc.value : null;
|
||||||
|
};
|
||||||
|
const track = async <T>(op: string, key: string, fn: () => Promise<T>): Promise<T> => {
|
||||||
|
const start = performance.now();
|
||||||
|
const res = await fn();
|
||||||
|
timings.push({ op, key, ms: performance.now() - start });
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
timings,
|
||||||
|
getObject: (key) => track("document", key, async () => stringifyObj(await getVal(key))),
|
||||||
|
getField: (key, field) => track("document", key, async () => (await getVal(key))?.[field] ?? null),
|
||||||
|
getIds: (key) => track("document", key, async () => ((await getVal(key)) || []).map(String)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdapter(db: string): Adapter {
|
||||||
|
if (db === "memcached") return memcachedAdapter();
|
||||||
|
if (db === "arangodb") return arangoAdapter();
|
||||||
|
return redisAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function timingSummary(timings: Timing[]) {
|
||||||
|
const total = timings.reduce((s, t) => s + t.ms, 0);
|
||||||
|
return { count: timings.length, total, ops: timings };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sumMs = (timings: Timing[]) => timings.reduce((s, t) => s + t.ms, 0);
|
||||||
|
|
||||||
|
const regionCache: Record<string, { id: string; region: any }> = {};
|
||||||
|
|
||||||
|
async function handlePlayer(adapter: Adapter, db: string) {
|
||||||
|
const player = await adapter.getObject("player:1");
|
||||||
|
if (!player || !player.id) return null;
|
||||||
|
|
||||||
|
const currentRegionId = player.region_id;
|
||||||
|
let region = null;
|
||||||
|
let regionCached = false;
|
||||||
|
|
||||||
|
if (currentRegionId) {
|
||||||
|
const cached = regionCache[db];
|
||||||
|
if (cached && cached.id === currentRegionId) {
|
||||||
|
region = cached.region;
|
||||||
|
regionCached = true;
|
||||||
|
} else {
|
||||||
|
const regionStart = sumMs(adapter.timings);
|
||||||
|
const regionName = await adapter.getField(`region:${currentRegionId}`, "name");
|
||||||
|
const enemiesIds = await adapter.getIds(`region:${currentRegionId}:enemies`);
|
||||||
|
const itemsIds = await adapter.getIds(`region:${currentRegionId}:items`);
|
||||||
|
const regionMs = sumMs(adapter.timings) - regionStart;
|
||||||
|
|
||||||
|
const enemies = [];
|
||||||
|
for (const id of enemiesIds) {
|
||||||
|
const s = sumMs(adapter.timings);
|
||||||
|
const enemy = await adapter.getObject(`enemy:${id}`);
|
||||||
|
if (enemy && enemy.id) {
|
||||||
|
enemy._ms = sumMs(adapter.timings) - s;
|
||||||
|
enemies.push(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
for (const id of itemsIds) {
|
||||||
|
const s = sumMs(adapter.timings);
|
||||||
|
const item = await adapter.getObject(`item:${id}`);
|
||||||
|
if (item && item.id) {
|
||||||
|
item._ms = sumMs(adapter.timings) - s;
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
region = { id: currentRegionId, name: regionName, enemies, items, _ms: regionMs };
|
||||||
|
regionCache[db] = { id: currentRegionId, region };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { player, region, regionCached };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUniverse(adapter: Adapter) {
|
||||||
|
const regions = [];
|
||||||
|
for (let i = 1; i <= 16; i++) {
|
||||||
|
const s = sumMs(adapter.timings);
|
||||||
|
const name = await adapter.getField(`region:${i}`, "name");
|
||||||
|
if (name) {
|
||||||
|
const enemiesIds = await adapter.getIds(`region:${i}:enemies`);
|
||||||
|
const itemsIds = await adapter.getIds(`region:${i}:items`);
|
||||||
|
regions.push({ id: i.toString(), name, enemiesIds, itemsIds, _ms: sumMs(adapter.timings) - s });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enemies = [];
|
||||||
|
for (let i = 1; i <= 30; i++) {
|
||||||
|
const s = sumMs(adapter.timings);
|
||||||
|
const enemy = await adapter.getObject(`enemy:${i}`);
|
||||||
|
if (enemy && enemy.id) {
|
||||||
|
const itemsIds = await adapter.getIds(`enemy:${i}:items`);
|
||||||
|
enemy.itemsIds = itemsIds || [];
|
||||||
|
enemy._ms = sumMs(adapter.timings) - s;
|
||||||
|
enemies.push(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
for (let i = 1; i <= 50; i++) {
|
||||||
|
const s = sumMs(adapter.timings);
|
||||||
|
const item = await adapter.getObject(`item:${i}`);
|
||||||
|
if (item && item.id) {
|
||||||
|
if (item.gear_type && item.gear_id) {
|
||||||
|
const gearStats = await adapter.getObject(`${item.gear_type}:${item.gear_id}`);
|
||||||
|
|
||||||
|
if (gearStats) {
|
||||||
|
if (gearStats.item_type_id) {
|
||||||
|
const typeInfo = await adapter.getObject(`item_type:${gearStats.item_type_id}`);
|
||||||
|
if (typeInfo && typeInfo.name) gearStats.Item_Type = typeInfo.name;
|
||||||
|
delete gearStats.item_type_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gearStats.item_effect_id) {
|
||||||
|
const effectInfo = await adapter.getObject(`item_effect:${gearStats.item_effect_id}`);
|
||||||
|
if (effectInfo && effectInfo.name) {
|
||||||
|
gearStats.Item_Effect = `${effectInfo.name} ${effectInfo.value ? `(+${effectInfo.value})` : ""}`;
|
||||||
|
}
|
||||||
|
delete gearStats.item_effect_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete gearStats.id;
|
||||||
|
item.stats = gearStats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item._ms = sumMs(adapter.timings) - s;
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { regions, enemies, items };
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = serve({
|
||||||
|
routes: {
|
||||||
|
"/*": index,
|
||||||
|
|
||||||
|
"/api/player": async (req) => {
|
||||||
|
const db = new URL(req.url).searchParams.get("db") || "redis";
|
||||||
|
try {
|
||||||
|
const adapter = getAdapter(db);
|
||||||
|
const result = await handlePlayer(adapter, db);
|
||||||
|
if (!result) {
|
||||||
|
return Response.json({ error: "Player not found", db }, { status: 404 });
|
||||||
|
}
|
||||||
|
return Response.json({ ...result, db, _timings: timingSummary(adapter.timings) });
|
||||||
|
} catch (e: any) {
|
||||||
|
return Response.json({ error: e.message, db }, { status: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"/api/universe": async (req) => {
|
||||||
|
const db = new URL(req.url).searchParams.get("db") || "redis";
|
||||||
|
try {
|
||||||
|
const adapter = getAdapter(db);
|
||||||
|
const result = await handleUniverse(adapter);
|
||||||
|
return Response.json({ ...result, db, _timings: timingSummary(adapter.timings) });
|
||||||
|
} catch (e: any) {
|
||||||
|
return Response.json({ error: e.message, db }, { status: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
development: process.env.NODE_ENV !== "production" && {
|
||||||
|
hmr: true,
|
||||||
|
console: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🚀 Server running at ${server.url}`);
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 338 B |
Loading…
x
Reference in New Issue
Block a user