From 8aeabcd2472b0409476bde089e184d6bb2847b5c Mon Sep 17 00:00:00 2001 From: SirCotare Date: Mon, 11 Jun 2012 18:52:19 +0200 Subject: [PATCH] #1470 monday's commit: update on parser (still incomplete); wip datasources; initial monitoring cronjob (done); various minor changes, fixes and so --HG-- branch : gsoc2012-achievements --- .../_AchWebParser/AchWebMonitor.php | 35 ++++ .../_AchWebParser/AchWebParser.php | 150 ++++++++++++++-- .../_AchWebParser/_doc/Class_scheme.dia | Bin 1867 -> 2199 bytes .../_AchWebParser/_doc/Class_scheme.png | Bin 5313 -> 9938 bytes .../_AchWebParser/class/Atom_class.php | 117 ++----------- .../class/DataSourceHandler_class.php | 44 +++-- .../class/DataSource_abstract.php | 13 +- .../_AchWebParser/class/Logfile_class.php | 22 +++ .../class/ParallelCURL_class.php | 160 ++++++++++++++++++ .../_AchWebParser/class/mySQL_class.php | 13 +- .../app_achievements/_AchWebParser/conf.php | 31 ++++ .../_AchWebParser/include/functions_inc.php | 15 +- .../_AchWebParser/log/_logDefaultDir_ | 0 .../app_achievements/_AchWebParser/parser.php | 132 +++++++++++++++ .../source/ValueCache/ValueCache_class.php | 19 +-- .../_AchWebParser/source/ValueCache/conf.php | 7 + .../source/XMLapi/XMLapi_class.php | 24 ++- .../_AchWebParser/source/XMLapi/conf.php | 8 + 18 files changed, 633 insertions(+), 157 deletions(-) create mode 100644 code/web/app/app_achievements/_AchWebParser/AchWebMonitor.php create mode 100644 code/web/app/app_achievements/_AchWebParser/class/Logfile_class.php create mode 100644 code/web/app/app_achievements/_AchWebParser/class/ParallelCURL_class.php create mode 100644 code/web/app/app_achievements/_AchWebParser/conf.php create mode 100644 code/web/app/app_achievements/_AchWebParser/log/_logDefaultDir_ create mode 100644 code/web/app/app_achievements/_AchWebParser/parser.php create mode 100644 code/web/app/app_achievements/_AchWebParser/source/ValueCache/conf.php create mode 100644 code/web/app/app_achievements/_AchWebParser/source/XMLapi/conf.php diff --git a/code/web/app/app_achievements/_AchWebParser/AchWebMonitor.php b/code/web/app/app_achievements/_AchWebParser/AchWebMonitor.php new file mode 100644 index 000000000..7a5895418 --- /dev/null +++ b/code/web/app/app_achievements/_AchWebParser/AchWebMonitor.php @@ -0,0 +1,35 @@ +connect($CONF['mysql_server'],$CONF['mysql_user'],$CONF['mysql_pass'],$CONF['mysql_database']); + + //check status + $res = $DBc->sendSQL("SELECT * FROM ach_monitor_status ORDER by ams_start DESC LIMIT 0,1","ARRAY"); + + if(($res[0]['ams_start'] < (time()-$CONF['timeout']) && $res[0]['ams_end'] == 0) || ($res[0]['ams_end'] > 0 && $res[0]['ams_end'] < (time()-$CONF['timeout']))) { + $fp = fsockopen($CONF['self_host'], 80, $errno, $errstr, 30); + if(!$fp) { + logf("ERROR: self call; socket: ".$errstr." (."$errno.")"); + } + else { + $out = "GET ".$CONF['self_path']." HTTP/1.1\r\n"; + $out .= "Host: ".$CONF['self_host']."\r\n"; + $out .= "Connection: Close\r\n\r\n"; + + fwrite($fp, $out); + fclose($fp); + } + } + + exit(0); +?> \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/AchWebParser.php b/code/web/app/app_achievements/_AchWebParser/AchWebParser.php index 84ec7273c..68cb415b5 100644 --- a/code/web/app/app_achievements/_AchWebParser/AchWebParser.php +++ b/code/web/app/app_achievements/_AchWebParser/AchWebParser.php @@ -2,20 +2,43 @@ error_reporting(E_ALL ^ E_NOTICE); ini_set("display_errors","1"); + if(file_exists("parser.stop")) { + exit(0); + } + require_once("class/mySQL_class.php"); require_once("conf.php"); require_once("inlcude/functions_inc.php"); $logfile = false; if($CONF['logging'] == true) { - $logfile = fopen($CONF['logfile'].'.'.date("Ymd",time()).'.txt','a'); + require_once("class/Logfile_class.php"); + #$logfile = fopen($CONF['logfile'].'.'.date("Ymd",time()).'.txt','a'); + $logfile = new Logfile($CONF['logfile']); + } + + //set mode: cron || single with given cid + #MISSING: conf to allow external calls; whitelist ips + $MODE = "CRON"; + if($_REQUEST["cid"] > 0 || $_REQUEST["invoke"] == "TRUE") { + if($_REQUEST["cid"] > 0 && $_REQUEST["invoke"] == "TRUE") { + $MODE = "SINGLE"; + $CID = $DBc->mre($_REQUEST["cid"]); + } + else { + $e = "Failed to start SINGLE mode; cid=".$_REQUEST["cid"]; + logf($e); + die($e); + } } //create database connection $DBc = new mySQL($CONF['mysql_error']); $DBc->connect($CONF['mysql_server'],$CONF['mysql_user'],$CONF['mysql_pass'],$CONF['mysql_database']); - #MISSING: mode -> single, cron, debug + if($MODE == "CRON") { + $RID = $DBc->sendSQL("INSERT INTO ach_monitor_state (ams_start,ams_end) VALUES ('".time()."','0')","INSERT"); // insert run into monitoring table + } require_once("class/DataSourceHandler_class.php"); require_once("class/DataSource_abstract.php"); @@ -30,26 +53,127 @@ $_DATA->registerDataSource($tmp); } + //synch chars from ring_open character table + if($CONF['synch_chars'] == true) { + $DBc_char = new mySQL($CONF['mysql_error']); + $DBc_char->connect($CONF['char_mysql_server'],$CONF['char_mysql_user'],$CONF['char_mysql_pass'],$CONF['char_mysql_database']); + + $DBc->sendSQL("UPDATE ach_monitor_character SET amc_confirmed='0'","NONE"); + + $res = $DBc_char->sendSQL("SELECT char_id,last_played_date FROM characters","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("INSERT INTO ach_monitor_character (amc_character,amc_last_import,amc_last_login,amc_confirmed) VALUES ('".$res[$i]['char_id']."','0','".dateTime_to_timestamp($res[$i]['last_played_date'])."','1') ON DUPLICATE KEY UPDATE amc_confirmed='1', amc_last_login='".dateTime_to_timestamp($res[$i]['last_played_date'])."'","NONE"); + } + + $DBc->sendSQL("DELETE FROM ach_monitor_character WHERE amc_confirmed='0'","NONE"); //remove deleted characters + //remove data for deleted chars + $DBc->sendSQL("DELETE FROM ach_player_atom WHERE NOT EXISTS (SELECT * FROM ach_monitor_character WHERE amc_character='apa_player')","NONE"); + $DBc->sendSQL("DELETE FROM ach_player_objective WHERE NOT EXISTS (SELECT * FROM ach_monitor_character WHERE amc_character='apo_player')","NONE"); + $DBc->sendSQL("DELETE FROM ach_player_perk WHERE NOT EXISTS (SELECT * FROM ach_monitor_character WHERE amc_character='app_player')","NONE"); + $DBc->sendSQL("DELETE FROM ach_player_valuecache WHERE NOT EXISTS (SELECT * FROM ach_monitor_character WHERE amc_character='apv_player')","NONE"); + } + #MISSING: fetch candidates + if($MODE == "SINGLE") { + $chars = array(); + $chars[] = array('amc_character',$CID); + } + else { + $chars = array(); + } + + + //fork if enabled in conf + if($CONF['fork'] == true && $MODE == "CRON") { + require_once("class/ParallelCURL_class.php"); + + $max_requests = 0; + $curl_options = array( + CURLOPT_SSL_VERIFYPEER => FALSE, + CURLOPT_SSL_VERIFYHOST => FALSE, + CURLOPT_USERAGENT, 'Ryzom - Achievement Tracker', + ); - foreach() { - #MISSING: fetch objectives to evaluate - foreach() { - #MISSING: fetch atoms - foreach() { - #MISSING: evaluate atoms + $_CURL = new ParallelCurl($max_requests, $curl_options); + + foreach($chars as $elem) { + $_CURL->startRequest("http://".$CONF['self_host']."/".$CONF['self_path']."?invoke=TRUE&cid=".$elem['amc_character'], 'received_char',null); + } + } + else { + $atom_list = array(); + + foreach($chars as $elem) { + #STEP 1: evaluate atoms + + //get unfinished perks which have no parent or complete parent + $res = $DBc->sendSQL("SELECT ap_id FROM ach_perk WHERE (ap_parent IS NULL OR EXISTS (SELECT * FROM ach_player_perk WHERE app_player='".$elem['amc_character']."' AND app_perk=ap_parent)) AND (NOT EXISTS (SELECT * FROM ach_player_perk WHERE app_player='".$elem['amc_character']."' AND app_perk=ap_id))","ARRAY"); + foreach($res as $perk) { + //get unfinished atoms belonging to unfinished objectives + $res = $DBc->sendSQL("","ARRAY"); + foreach($res2 as $atom) { + if(!isset($atom_list[$atom['atom_id']])) { // only load if not already cached + $atom_list[$atom['atom_id']] = new Atom($atom); + } + + $atom_list[$atom['atom_id']]->evalRuleset($elem['amc_character']); + } + } + + #STEP 2: detect obj/perk progression + //obj + $res = $DBc->sendSQL("SELECT ao_id FROM ach_objective WHERE ao_condition='all' AND NOT EXISTS (SELECT * FROM ach_atom WHERE atom_objective=ao_id AND NOT EXISTS (SELECT * FROM ach_player_atom WHERE apa_atom=atom_id AND apa_state!='GRANT' AND apa_player='".$elem['amc_character']."'))","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("INSERT INTO ach_player_objective (apo_objective,apo_player,apo_date) VALUES ('".$res[$i]['ao_id']."','".$elem['amc_character']."','".time()."')","NONE"); + } + + $res = $DBc->sendSQL("SELECT ao_id FROM ach_objective WHERE ao_condition='value' AND ao_value<=(SELECT count(*) FROM ach_atom WHERE atom_objective=ao_id AND EXISTS (SELECT * FROM ach_player_atom WHERE apa_atom=atom_id AND apa_state='GRANT' AND apa_player='".$elem['amc_character']."'))","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("INSERT INTO ach_player_objective (apo_objective,apo_player,apo_date) VALUES ('".$res[$i]['ao_id']."','".$elem['amc_character']."','".time()."')","NONE"); + } + + $res = $DBc->sendSQL("SELECT ao_id FROM ach_objective WHERE ao_condition='any' AND EXISTS (SELECT * FROM ach_atom WHERE atom_objective=ao_id AND EXISTS (SELECT * FROM ach_player_atom WHERE apa_atom=atom_id AND apa_state='GRANT' AND apa_player='".$elem['amc_character']."'))","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("INSERT INTO ach_player_objective (apo_objective,apo_player,apo_date) VALUES ('".$res[$i]['ao_id']."','".$elem['amc_character']."','".time()."')","NONE"); + } + + //perk + $res = $DBc->sendSQL("SELECT ap_id FROM ach_perk WHERE ap_condition='all' AND NOT EXISTS (SELECT * FROM ach_objective WHERE ao_perk=ap_id AND NOT EXISTS (SELECT * FROM ach_player_objective WHERE apo_objective=ao_id AND apo_state!='GRANT' AND apo_player='".$elem['amc_character']."'))","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("INSERT INTO ach_player_perk (app_objective,app_player,app_date) VALUES ('".$res[$i]['ap_id']."','".$elem['amc_character']."','".time()."')","NONE"); + } + + $res = $DBc->sendSQL("SELECT ap_id FROM ach_perk WHERE ap_condition='value' AND ap_value<=(SELECT count(*) FROM ach_objective WHERE ao_perk=ap_id AND EXISTS (SELECT * FROM ach_player_objective WHERE apo_objective=ao_id AND apo_state='GRANT' AND apo_player='".$elem['amc_character']."'))","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("INSERT INTO ach_player_perk (app_objective,app_player,app_date) VALUES ('".$res[$i]['ap_id']."','".$elem['amc_character']."','".time()."')","NONE"); + } + + $res = $DBc->sendSQL("SELECT ap_id FROM ach_perk WHERE ap_condition='any' AND EXISTS (SELECT * FROM ach_objective WHERE ao_perk=ap_id AND EXISTS (SELECT * FROM ach_player_objective WHERE apo_objective=ao_id AND apo_state='GRANT' AND apo_player='".$elem['amc_character']."'))","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("INSERT INTO ach_player_perk (app_objective,app_player,app_date) VALUES ('".$res[$i]['ap_id']."','".$elem['amc_character']."','".time()."')","NONE"); } - #MISSING: evaluate objective } - #MISSING: evaluate perk } - + if($CONF['sleep_time'] != false) { + sleep($CONF['sleep_time']); + } + if($logfile) { + $logfile->write(); + } + //self call if cron mode is on + if($MODE == "CRON" && $CONF['enable_selfcall'] == true) { + $DBc->sendSQL("UPDATE ach_monitor_state SET ams_end='".time()."' WHERE ams_id='".$RID."'","NONE"); - #MISSING: self call on cron mode - if() { $fp = fsockopen($CONF['self_host'], 80, $errno, $errstr, 30); if(!$fp) { logf("ERROR: self call; socket: ".$errstr." (."$errno.")"); diff --git a/code/web/app/app_achievements/_AchWebParser/_doc/Class_scheme.dia b/code/web/app/app_achievements/_AchWebParser/_doc/Class_scheme.dia index b620e6af477dfadc2eb501fcf7f8829a31df2c56..2295a2aff9c85d74e32159ea43f5538ac413e7a5 100644 GIT binary patch literal 2199 zcmZ|EcQhM{0tWD^)ruXoW{jYXP@`A0W~@-jEo!uCQ{x$}(%53uEVW8)YKPiY#Hv-a zv_^;(BZ^X@gmA^U=e_gJxqrO#zCXV6o!-530@$&!@ts%Z3;kX=Rzhn~zvH%@x_Q&IFL^3iO%&*4L_sSX%gBt8~p^5z(eA0AK4H(>IKGg^oW_NkNW zx>%vyV-?9p2JkGhp&VJ6p$8?598v8npze#qeAUyH^U))~pWTz15vA1;UE;_IfB(KR z(>?!^@ySUbSMJLy1MVII*rDXYMC_*lOn_~7iIwS=>O*6_$?xbw(=u>j9k1X^zX5WL z?-qu|`2+j4KCfyURZZ?xAvIGUSO_hpGp57_R${H`dh*)+(|X$Y{WTlGleIt;?sc{r z8O@8hNGls-Ev9<4Pm7L4vWn+HihZE5*HTW+g+0JCB#wlHH-gOc24<7z~q zB&*D_XouihR~6vk!q-xbg;U%!67JbP?wMNlK&n~VbMu#Zh*bOA3&oH&mr)wqYVGG^(miv8L z_4N8IJ|0%iAx;9LDOvW_*`VQiH$gvG9xGTPm6SqdAf+~>2H%mb&rD=CGG})IwgBrT zd~do!&q_DQn#Iirv&;f=4C&Hd6?fg@3-+P+HlFgkcwDI|l;NvdJgM*vt+5z$3ip+H zD$7-~`mhn4EqtDGz|QO5o&y;Lb;5Vv)+a=Ld2EXchp=PS$Gp1^rP`zoSI-gY@5Y#G z3Y0L00NtRXaYLQ7MML*;Sx03clr=&3#Pf^Fjyw-rWXB3|lYThnnZr%_xNKL~K2N$# z81`O>cgae6;Ig*0{?OCwcpc;IrY(O~htg03h2Xe^j>0hIfFc<=W&b{N9(_Y(sF$PU zztZO(&soo*_iXEY^M+lHX@9JICBlW`MRek|P9XhrgMvE}mmNdU`G#FSw5RxPi`UL>z99q1 zYyCB%U$Ad6C_(l{{B1QVg&^bOX1&1O)+!eAgA1G;`=C)a#N}3*XY*H8*pyI;l$O=W zuV13PHdP{sjGQU!XU%sQ2YU~<`ok}(QeK3wC_2?qaNj%6CF(0iz${&9tpocrpfH_V0*|KXFc-_{Wcxd z?XyU|);Y5LjtYa&1Y0Z~u>IZ<&ihMd;^VI7a0S4t$v&rSAj@mg5&5>2nq>H1gW_nK z!m~QPALlR&6KSB0B)O}WLxvT~;UgAjc5|`l_zaHLoKUHLYcZh9_a-}pK9YiG0QZZE zF3tHnPqiEnpZ!J` zz@TB8t-#0Eg?#j-^;y~W)${}4nDTty;YJ39H?BCS<3yC+G zDvcQ@tk)$!#=O8!zLwoPIa{oBtq`pZ_1Ii0FI&2^lTcuHPIb$5FyEW;A|YjIjA9S# z7Mw0zb2O+0Cq_(l!nUd%=zyKyb1Th#9{^PVVH-D=xMm!o_0 zDYDHe*0QAnvbnn*rA~Do}My#XjkX z*~eRA{d?;f3E@!5FZN_LQkRP1hc;1;73cN!9zAOBq1f%lO!g+K`qZ?wqw~6_p&|i+ zemcTwjb~N{fyl&7KB=1FjTwGv!6k0#-2R4dE%@gOTetJVlY0i2SDiFIo~*7mj!l1% zWlNUs?`Vjhm0(PM&^2{ECR9~n2wVnk*%-2DH!;8Jg^FLB5e^v5(0;t$8k}@yq9?=9 zz}c_O34lZYXS}zxVKA>{G|JNUx{EK@Q6I!vDZRoXXD)xCUyIl`vk+mAj_B2LsFE2` z?(G4Yz^?~IvGiKXbY}4TcfZ&Mtz3F&M%13Fd0G|(n$)3>D)I(hXJDPEPpB^yYo(uN zZlzx^d8^6??O$OmVmD^4EJ26M0G>vuX^Dt(q#z{ri?qWvKY^Axej`xl4`apmHUGK7 z59uz`MTBeRl*O~V*znQ8eR=Y7%ciJDjbg-MO05E|tmpwQBfAwrpXQOO@ zXlZJtRwJR|Lp%-SSeka=m~KO3L)2sozwB6fHWf9hKwe=6G6wQLW(>6KG7{wjFI;H) V7Yu2& literal 1867 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 literal 5313 zcmdT|cTiJXw?9-76l`3iU+*U>NL3*8fT#$eN`fd-a|P)rQUe4ma4mo$9fY7lic}#% zKnQ{oL5k8%h{%n!KmsTPAtreTzx&PHd2`>)dw;&kWX_zu_t|@$wbt*q*6-YQcC^_k zsUQgez)ssUC(i%2eUZ|2)2HhR_sFn*cWYk(h441%%YPs zCr%2sEq~N`a7I?xV2_O0G~GMm!I2X%)g$=1`U$4wfO9R+H1*2@xg4L#x zAJ*l!wM>y*Hkm3_^qoFd8lcH=TW7H|Mk<*}8eErCw|KkYLI5rc zBMDgMh#~-ha4k6YA-deZeU#*9Sy@9|i`4F8q?@;uaq~va`BYv?p|bQgv?3rdfSp>4 zFtYdRxs+j87!3eMi63Q0PESI3hm@*4x1T{UMcsXzf?}a=^5PX5(uBke3i4Pb=hkbE zA&W~BXom*iG83;-d$+gO4N>bT54r9YW-|wx$a$Ql7@R7dWw&WE6Z0Ux6#%fIxfq2L zX1%X93d$!*Flx85i1E2eJF2!ZTY>kE_xM9&X!jD@4sA!hRdj4MreG5#zgcv&2|9Zh z-7nWurjueEx*qDfX(q%q^l(n^u4>+S>)l{9bv-lg;Uh-h^VL4`(_ zmwTD{wlI8>^qLNqHExLPS`y(J&`bz*w8*w}Y?w}904{w3IpZjyV7d~-yuu|tivMAJ z=z&9E44xusVCQhUC$2T`X1^YH);_nezSOe7TsG?Y4dytcI1BP(Snn#W;28H>Z!|K1 z+}}jMcyvd$S#H*(Z&-`PcOaQ`z6&P|MATCmO^uvyU%DUey1tdty#ChV%*y^B)4uw&6)3nun!%OxYTOy^w484$#Vs%0?E34jaz(Oj9j+=N zd$?o!o?K_XFS282Nf`Ux4Z!lDvnxV=c3lC)fa#cU7>|5yvRG#xUyi&=Qrzq2O6g(A z)K(62o!0s6Xjnn$y`Z4^>^kJr=1_gVcLlwP3Ca2D-Y;DnbL>y#M>ajnV)-rgxn`*4 zcU;3CH%aH@QQLG2sn{h>`KcR0%!W<%1g@;TP_lW!g4EnN2^PSeZIItdC|9m#4aYkq z3IUi2+<8Iy@t#zYPE`^IaSbMr0W9gK(_NHJ)>IY0tJ2WD`v=FL-Y8~>0M6tO5AhWl zmyQ(dFqqkYT^(o-tp5xJ0K}dx5*-X#zzK&uxShYYxiMQh#ul#C1#|ARC~NBO>Tg(n zk^Fky;fT4UG_aD#f7guPWYqV^w%pwfcw}SCf-QtlSCR_;PgTCeMPJPp9p4bq>3t# zyrvbseFRTssb*3j1Z-j%{ES$xPgGX?c^g@hjsiT*@fqryIN&~@^4$H}F$C&%a&^Vy z0;;%=)Tr0vHv=^2sLL&0{-sgDp4)9G0=dd`pAX&w0EUBDI_WUJ{sJyT;R$u;oy1pq zB+q_c9}CG51AG+yi|l2k?pO<5T}*1C0YF5d=6BGQ|E?*^9Xq6KZQydoU zz|CFNH8q_hBO`^L!f)N8oDIUFxy|Q(Rcuy%8xs{J3r<=~9574V)Sn=up*NSw%aYD5 zgd2pR2JZWH6sIL^zR)sWz~@C=xw5;B%Wm0qouCn6LXal7Y9Ss36B1dab*xJIHlHHu z2uL1Y7cb%n!iUx5;kQa(Fjl^I4LDewW8Jn2Uh|bs>pXDY*!&GelsM<+sz@NTtK0T( zI*Hv5OP&im7mr32aT1Xks!|YqsNJS2%B#7NqGIDHNad)S7q&g$S9N&0Y5lv_G48)%+S2tT7 zjq>j2mD|zY8uPO)924!Wv9GOTQN=+ag1W@p#1H4DHF-RdcXNtG)Jv5b5gMN)33N&C z%0D5$ci*;f5DT~C!kkWCXhzfTT&;J~vR}O(wFB|*S9cE?MMKW?c_NBT9$fC`ukCbj zj~7r0xiA4grEwbm*CI`vbUJ=fama|o(zEMIm?7nvt_0iDtXU3mNtByEvL?e9_gj1G z&J>;zLp|_t2nBslgn>?F8}0hd&VK}wq825I>aG0R#>if+IT)L3);7prV?l@0)CbU! zg7?9G%t}ESqgNi@w;8aI>c2^G2Z8)?4r79{fe%CW6Bg{yE>?kpBXkSg&N{ZM#DJK0`p)B{CF5M&x_$U?|_}wEUj!^Z7 zmV5XC6p(yrJ3&5GTSy9M|KXxx`y5Yj#X6Lz<6)BCyKM>!1a z`s^?dq_0nNQE=qmT))OX7a@mq$RlcWY1sj(E}}4?dHB3b`mNeS?Zm?lQIzM=H52l*-|d1?but z89n*v8E+zSc1{EU7?lce*UeF_Ry=0B?f$C^iTRz*Xk_qf@HKs|IV%&4DylVQTYdxu z;(D402m)_7(Hi?MO=Z>pm%Ox<1Yad)N@TGs{h>r3}Bsk9H28w3r@F&{UdfD5XLJ zCWjeEcP|7p1zXIGaeH#um1zCJdF{$=z<}6S#tfAZNF^s8|1uk!P-nP#B7i9n)&Ocx zeYnFMe&(pvy82<5lg3#x9r1D<85}$dw2z*;!OW6R>436Pc>u{c7{$pkDCa zz{LgRFuTTk$HB|#|0ou4P!PAl5*Vg=M+rO!Rs3BU9AE>7pxYa64&X=eUQn@2}O?O zbV-v00Z?3jwN3TrF8)r!BwE1hY4w#!+MLHObC<|9^qq$7zk`+${@IePeLcOsYv^Lh zE}BkaM~#09vqy1j%n_<5aOCmhE#U=+`(C0juCTOguVku1rM;6jE`9&=uFfc1(sH-# zmMHt5_WoZxt&3WU!;2@UL7#=V@3rJs`x(Gf?s;wyMvwX_XWFEokj0zZk{^LY-u<~bwJqs& z>Yi(jtkknuexuDPS-`Mb@yFG!vMym(v2X2Dzao?(Z7U&6@4G&D8JI|Mx;4;d7u;gk z%K=l+vgXi)p>G7+bwN$`16z-K9YSLaLxDw0)89vR9T9I2rh!eOVxHtJ>y$dhpjnH) zPoe?GIrIO*3AZ~o-0(>+t3Bxa>@E9-9-+OOHQ&izVNj;Pe{c-)3fM!)$jQkC$2?KM z+g*;?5!9RStjML^(NZ@c^(lxzzU_t^+j>aKVfpT#P)n`LW8P@+YXZh5P~v6xdpVC= zzsg1gdk~YSK>)tm_UV_cqWCg)#g7#RSDTHZLyD5XE`SXkCXpE|aHqge*`wdUf)=sP zrW0F)+EWYc#eQNyC^^VDk`(?o#Dn7I;1{p-fL;VDw|UU1U#}&1u~Q*)@3^>CPG3)} zrcVsFDowoSi{>9m8df251SoM`bzIddhkk*3 zA3uJi(to{y>6w{MI#PqMO-DrE8W}pbX#zv|BiE;ukVl?MD@Rtda)b7cYg@_kpDBt` z1*+UB>Ze9pSYm3{9LRRoow>qxV6LKVlS9Hv7mT&cyYg4+twZxxeQz6XzKjkn4K1o5Z%R!l9uue*TJx`wdjmH z+O;}hzb@n|%-WMvnuXLaYv4VkX@@fvn_c3;wy`)~kiv3yx%AE6%})!m+Xwb6JF>*q zJ&Ww{tEem@8kZ~6UN()Ni7-=n0-dN-ClBe>b}H_C=QxS%C9Ta2q3c;F0)yYB*meb`)bq)kpYDK0l?Iq?FEIchMt*c2$Nu^ay;#{1o{C* zc*YL;x%lEH>S3+*rgz&>!J-|;+)~<1{^xmv3tiuGC?*7~jq{~?JE$4Ve`vPEalkmr zq$|7D<$1ac6c{~->$nHk74os@!gbgecZep(;lYvI9p%D00P4wKtSj{S_4(+Gsruleset = $data['atom_ruleset']; - $this->ruleset_parsed = false; + $this->ruleset = $data['atom_ruleset_parsed']; $this->id = $data['atom_id']; $this->objective = $data['atom_objective']; } - private function parseRuleset() { - #WORKPAD:#### - /* - Trigger: - by value - (by event) - - Sources: - XML - valuecache - ring_open - (Achievement Service) - (Mirror Service) - - Keywords: - VALUE - GRANT:EVENT player_death - DENY:TIMER 3600 - RESET - RESET_ALL - UNLOCK - UNLOCK_ALL - - IF - SCRIPT - MSG - - VALUE dappers = c_money - IF(dappers >= 5000) { - GRANT - } - - VALUE tmp = c_fame[scorchers] - IF(tmp == 0) { - DENY:3600 - } - - VALUE x = c_pos_x - VALUE y = c_pos_y - SCRIPT inside(x,y) { - IF(MSG == "Majestic Garden") { - GRANT - } - } - - EVENT player_death - ON player_death { - UNLOCK - } - - EVENT region_changed - ON region_changed { - IF(MSG == "Majestic Garden") { - GRANT - } - } - */ - ############# - - - VALUE var = name - - IF(statement) { - - } - - SCRIPT script(a,r,g,s) { - MSG - } - - EVENT name - - ON name { - MSG - } - - GRANT - GRANT:EVENT name - GRANT:TIMER seconds - - DENY - DENY:EVENT name - DENY:TIMER seconds - - RESET - RESET_ALL - UNLOCK - UNLOCK_ALL - } - function evalRuleset($user) { global $DBc,$_DATA; - if($this->ruleset_parsed == false) { - $this->parseRuleset(); - } - try { - return eval($this->ruleset_parsed); + return eval($this->ruleset); } catch(Exception $e) { return $e->getMessage() @@ -137,8 +40,14 @@ $DBc->sendSQL("DELETE FROM ach_player_atom WHERE apa_atom='".$this->id."' AND apa_player='".$user."'","NONE"); } - private function reset_all() { + private function reset_all($user) { + global $DBc; + $res = $DBc->sendSQL("SELECT atom_id FROM ach_atom WHERE atom_objective='".$this->objective."'","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("DELETE FROM ach_player_atom WHERE apa_atom='".$res[$i]['atom_id']."' AND apa_player='".$user."'","NONE"); + } } private function unlock($user) { @@ -147,8 +56,14 @@ $DBc->sendSQL("DELETE FROM ach_player_atom WHERE apa_atom='".$this->id."' AND apa_player='".$user."' AND apa_state='DENY'","NONE"); } - private function unlock_all() { + private function unlock_all($user) { + global $DBc; + $res = $DBc->sendSQL("SELECT atom_id FROM ach_atom WHERE atom_objective='".$this->objective."'","ARRAY"); + $sz = sizeof($res); + for($i=0;$i<$sz;$i++) { + $DBc->sendSQL("DELETE FROM ach_player_atom WHERE apa_atom='".$res[$i]['atom_id']."' AND apa_player='".$user."' AND apa_state='DENY'","NONE"); + } } function getID() { diff --git a/code/web/app/app_achievements/_AchWebParser/class/DataSourceHandler_class.php b/code/web/app/app_achievements/_AchWebParser/class/DataSourceHandler_class.php index 53b64d8d6..8c446d25f 100644 --- a/code/web/app/app_achievements/_AchWebParser/class/DataSourceHandler_class.php +++ b/code/web/app/app_achievements/_AchWebParser/class/DataSourceHandler_class.php @@ -6,36 +6,50 @@ function DataSourceHandler() { $this->source = array(); $this->alloc = array(); - - } function registerDataSource($src) { $i = sizeof($this->source); $this->source[$i] = $src; foreach($src->getTypes() as $elem) { - if(!is_array($this->alloc[$elem])) { - $this->alloc[$elem] = array(); - } //add to list - $this->alloc[$elem][$src->getPriority($elem)] = $i; + $this->alloc[$elem] = $i; + } + } + + function getData($ident,$field) { + $type = false; + $tmp = $this->getDataSource($field,$type); + if($tmp == false) { + return false; } + return $tmp->getData($field,$ident,$type); } - function getData($type,$field,$ident) { - return $this->getDataSource($type)->getData($type,$field,$ident); + function writeData($ident,$field,$data) { + $type = false; + $tmp = $this->getDataSource($field,$type); + if($tmp == false) { + return false; + } + return $tmp->writeData($field,$ident,$data,$type); } - private function getDataSource($type) { - //find the highest priority datasource for given type + + private function getDataSource(&$field,&$type) { + $type = $field; + //allow wildcard datafields + $tmp = explode(":",$field); + if(sizeof($tmp) > 1) { + $field = $tmp[1]; + $type = $tmp[0]."*"; + } + if(!$this->alloc[$type]) { return false; //unknown type } - $pos = array_keys($this->alloc[$type]); - if(sizeof($pos) == 0) { - return false; //no datasource for type // should not happen since type is defined by datasource - } - return $this->alloc[$type][$pos[0]]; + + return $this->source[$this->alloc[$type]]; } } ?> \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/class/DataSource_abstract.php b/code/web/app/app_achievements/_AchWebParser/class/DataSource_abstract.php index 4b5d64bf6..02453f06e 100644 --- a/code/web/app/app_achievements/_AchWebParser/class/DataSource_abstract.php +++ b/code/web/app/app_achievements/_AchWebParser/class/DataSource_abstract.php @@ -1,24 +1,25 @@ types = $CONF["types"]; + $this->write = $CONF["write"]; } function getTypes() { return $this->types; } - function getPriority($type) { - return $this->priority[$type]; + function isWriteable() { + return $this->write; } - abstract function getData($type,$ident,$field = array()); + abstract function getData($ident,$field,$type); - abstract function writeData($type,$ident,$field = array(),$value = array()); + abstract function writeData($ident,$field,$data,$type); } ?> \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/class/Logfile_class.php b/code/web/app/app_achievements/_AchWebParser/class/Logfile_class.php new file mode 100644 index 000000000..58254ff3c --- /dev/null +++ b/code/web/app/app_achievements/_AchWebParser/class/Logfile_class.php @@ -0,0 +1,22 @@ +logfile = $f; + $this->buffer = ""; + } + + function append($t) { + $this->buffer .= $t; + } + + function write() { + $f = fopen($this->logfile.'.'.date("Ymd",time()).'.txt','a'); + fwrite($f,$this->buffer); + fclose($f); + $this->buffer = ""; + } + } +?> \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/class/ParallelCURL_class.php b/code/web/app/app_achievements/_AchWebParser/class/ParallelCURL_class.php new file mode 100644 index 000000000..233d7a101 --- /dev/null +++ b/code/web/app/app_achievements/_AchWebParser/class/ParallelCURL_class.php @@ -0,0 +1,160 @@ +startRequest('http://example.com', 'on_request_done', array('something')); +// +// The first argument is the address that should be fetched +// The second is the callback function that will be run once the request is done +// The third is a 'cookie', that can contain arbitrary data to be passed to the callback +// +// This startRequest call will return immediately, as long as less than the maximum number of +// requests are outstanding. Once the request is done, the callback function will be called, eg: +// +// on_request_done($content, 'http://example.com', $ch, array('something')); +// +// The callback should take four arguments. The first is a string containing the content found at +// the URL. The second is the original URL requested, the third is the curl handle of the request that +// can be queried to get the results, and the fourth is the arbitrary 'cookie' value that you +// associated with this object. This cookie contains user-defined data. +// +// By Pete Warden , freely reusable, see http://petewarden.typepad.com for more + +class ParallelCurl { + + public $max_requests; + public $options; + + public $outstanding_requests; + public $multi_handle; + + public function __construct($in_max_requests = 10, $in_options = array()) { + $this->max_requests = $in_max_requests; + $this->options = $in_options; + + $this->outstanding_requests = array(); + $this->multi_handle = curl_multi_init(); + } + + //Ensure all the requests finish nicely + public function __destruct() { + $this->finishAllRequests(); + } + + // Sets how many requests can be outstanding at once before we block and wait for one to + // finish before starting the next one + public function setMaxRequests($in_max_requests) { + $this->max_requests = $in_max_requests; + } + + // Sets the options to pass to curl, using the format of curl_setopt_array() + public function setOptions($in_options) { + + $this->options = $in_options; + } + + // Start a fetch from the $url address, calling the $callback function passing the optional + // $user_data value. The callback should accept 3 arguments, the url, curl handle and user + // data, eg on_request_done($url, $ch, $user_data); + public function startRequest($url, $callback, $user_data = array(), $post_fields=null) { + + if( $this->max_requests > 0 ) + $this->waitForOutstandingRequestsToDropBelow($this->max_requests); + + $ch = curl_init(); + curl_setopt_array($ch, $this->options); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); + + if (isset($post_fields)) { + curl_setopt($ch, CURLOPT_POST, TRUE); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields); + } + + curl_multi_add_handle($this->multi_handle, $ch); + + $ch_array_key = (int)$ch; + + $this->outstanding_requests[$ch_array_key] = array( + 'url' => $url, + 'callback' => $callback, + 'user_data' => $user_data, + ); + + $this->checkForCompletedRequests(); + } + + // You *MUST* call this function at the end of your script. It waits for any running requests + // to complete, and calls their callback functions + public function finishAllRequests() { + $this->waitForOutstandingRequestsToDropBelow(1); + } + + // Checks to see if any of the outstanding requests have finished + private function checkForCompletedRequests() { + + // Call select to see if anything is waiting for us + if (curl_multi_select($this->multi_handle, 0.0) === -1) + return; + + // Since something's waiting, give curl a chance to process it + do { + $mrc = curl_multi_exec($this->multi_handle, $active); + } while ($mrc == CURLM_CALL_MULTI_PERFORM); + + // Now grab the information about the completed requests + while ($info = curl_multi_info_read($this->multi_handle)) { + + $ch = $info['handle']; + $ch_array_key = (int)$ch; + + if (!isset($this->outstanding_requests[$ch_array_key])) { + die("Error - handle wasn't found in requests: '$ch' in ". + print_r($this->outstanding_requests, true)); + } + + $request = $this->outstanding_requests[$ch_array_key]; + + $url = $request['url']; + $content = curl_multi_getcontent($ch); + $callback = $request['callback']; + $user_data = $request['user_data']; + + call_user_func($callback, $content, $url, $ch, $user_data); + + unset($this->outstanding_requests[$ch_array_key]); + + curl_multi_remove_handle($this->multi_handle, $ch); + } + + } + + // Blocks until there's less than the specified number of requests outstanding + private function waitForOutstandingRequestsToDropBelow($max) + { + while (1) { + $this->checkForCompletedRequests(); + if (count($this->outstanding_requests)<$max) + break; + + usleep(10000); + } + } + +} + + +?> \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/class/mySQL_class.php b/code/web/app/app_achievements/_AchWebParser/class/mySQL_class.php index 4202ffcd8..dba981254 100644 --- a/code/web/app/app_achievements/_AchWebParser/class/mySQL_class.php +++ b/code/web/app/app_achievements/_AchWebParser/class/mySQL_class.php @@ -19,7 +19,7 @@ function mySQL($err=false) { $this->DBstats = array(); $this->DBc = false; - if($err === "DIE" || $err === "PRINT" || $err === "ALERT" || $err === "HIDE") { + if($err === "DIE" || $err === "PRINT" || $err === "ALERT" || $err === "HIDE" || $err === "LOG") { $this->DBerror = $err; } else { @@ -31,13 +31,16 @@ function connect($ip,$user,$pass,$db=false) { $this->DBc = mysql_pconnect($ip,$user,$pass) or $this->error(mysql_error()); - if($db) { + if($this->DBc && $db) { $this->database($db); } $this->resetStats(); } function database($db) { + if(!$this->DBc) { + return false; + } mysql_select_db($db,$this->DBc) or $this->error(mysql_error()); } @@ -54,6 +57,9 @@ #if($this->cached !== false) { #$this->unlinkSql($this->cached); #} + if(!$this->DBc) { + return false; + } if($buffer === false && $handling !== "PLAIN") { $res = mysql_unbuffered_query($query,$this->DBc) or $this->error(mysql_error(),$query); @@ -142,6 +148,9 @@ case 'ALERT': echo ""; break; + case 'LOG': + logf("MySQL ERROR: ".$error); + break; default: flush(); break; diff --git a/code/web/app/app_achievements/_AchWebParser/conf.php b/code/web/app/app_achievements/_AchWebParser/conf.php new file mode 100644 index 000000000..23401b223 --- /dev/null +++ b/code/web/app/app_achievements/_AchWebParser/conf.php @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/include/functions_inc.php b/code/web/app/app_achievements/_AchWebParser/include/functions_inc.php index 17e9a89ae..969ffc2f1 100644 --- a/code/web/app/app_achievements/_AchWebParser/include/functions_inc.php +++ b/code/web/app/app_achievements/_AchWebParser/include/functions_inc.php @@ -5,7 +5,7 @@ if($nl) { $txt .= "\n"; } - fwrite($logfile,"[".date('H:i:s',time())."] ".$txt); + $logfile->append("[".date('H:i:s',time())."] ".$txt); } } @@ -16,4 +16,17 @@ } return $tmp."> ".$txt; } + + function dateTime_to_timestamp($dt) { + #2012-05-12 00:26:40 + $tmp = explode(" ",$dt); + $d = explode("-",$tmp[0]); + $t = explode(":",$tmp[1]); + + return mktime($t[0],$t[1],$t[2],$d[1],$d[2],$d[0]); + } + + function received_char($res, $url, $ch, $argv) { + logf("character tracking returned: ".$res); + } ?> \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/log/_logDefaultDir_ b/code/web/app/app_achievements/_AchWebParser/log/_logDefaultDir_ new file mode 100644 index 000000000..e69de29bb diff --git a/code/web/app/app_achievements/_AchWebParser/parser.php b/code/web/app/app_achievements/_AchWebParser/parser.php new file mode 100644 index 000000000..65836d1ee --- /dev/null +++ b/code/web/app/app_achievements/_AchWebParser/parser.php @@ -0,0 +1,132 @@ +private function parseRuleset() { + $this->ruleset_parsed = $this->ruleset; + #WORKPAD:#### + /* + Trigger: + by value + (by event) + + Sources: + XML + valuecache + ring_open + (Achievement Service) + (Mirror Service) + + Keywords: + VALUE + GRANT:EVENT player_death + DENY:TIMER 3600 + RESET + RESET_ALL + UNLOCK + UNLOCK_ALL + + IF + SCRIPT + MSG + + VALUE dappers = c_money + IF(dappers >= 5000) { + GRANT + } + + VALUE sum = c_cache:sum + IF(sum > 1000) { + GRANT + } + + VALUE tmp = c_fame[scorchers] + IF(tmp == 0) { + DENY:3600 + } + + VALUE x = c_pos_x + VALUE y = c_pos_y + SCRIPT inside(x,y) { + IF(MSG == "Majestic Garden") { + GRANT + } + } + + EVENT player_death + ON player_death { + UNLOCK + } + + EVENT region_changed + ON region_changed { + IF(MSG == "Majestic Garden") { + GRANT + } + } + */ + ############# + + + #VALUE var = name; + $match = array(); + preg_match_all("#VALUE ([a-zA-Z0-9_]) ?= ?([a-zA-Z0-9_]);#", $this->ruleset_parsed, $match,PREG_PATTERN_ORDER); + foreach($match[0] as $key=>$elem) { + $tmp = '$'.$match[1][$key].' = $_DATA->getData("VALUE","'.$match[2][$key].'",$user);\n'; + $tmp .= 'if($'.$match[1][$key].' == ) {\n'; + $tmp .= 'ERROR\n'; + $tmp .= '}\n'; + $this->ruleset_parsed = str_replace($elem,$tmp,$this->ruleset_parsed); + } + + + #IF(statement) { } + $match = array(); + preg_match_all("#IF ?\(([^\)]*)\) ?{#", $this->ruleset_parsed, $match,PREG_PATTERN_ORDER); + foreach($match[0] as $key=>$elem) { + $tmp = 'if() {\n'; + $this->ruleset_parsed = str_replace($elem,$tmp,$this->ruleset_parsed); + } + + + SCRIPT script(a,r,g,s) { + MSG + } + + #EVENT name; + $match = array(); + preg_match_all("#EVENT ([^;]*);#", $this->ruleset_parsed, $match,PREG_PATTERN_ORDER); + foreach($match[0] as $key=>$elem) { + $tmp = ''; + $this->ruleset_parsed = str_replace($elem,$tmp,$this->ruleset_parsed); + } + + ON name { + MSG + } + + #GRANT; + #GRANT:EVENT name; + #GRANT:TIMER seconds; + $match = array(); + preg_match_all("#GRANT:?([^;]*);#", $this->ruleset_parsed, $match,PREG_PATTERN_ORDER); + foreach($match[0] as $key=>$elem) { + $tmp = '$this->grant("'.$match[1][$key].'");'; + $this->ruleset_parsed = str_replace($elem,$tmp,$this->ruleset_parsed); + } + + #DENY; + #DENY:EVENT name; + #DENY:TIMER seconds; + $match = array(); + preg_match_all("#DENY:?([^;]*);#", $this->ruleset_parsed, $match,PREG_PATTERN_ORDER); + foreach($match[0] as $key=>$elem) { + $tmp = '$this->deny("'.$match[1][$key].'");'; + $this->ruleset_parsed = str_replace($elem,$tmp,$this->ruleset_parsed); + } + + #RESET; + #RESET_ALL; + #UNLOCK; + #UNLOCK_ALL; + $this->ruleset_parsed = str_replace("RESET_ALL;",'$this->reset_all();',$this->ruleset_parsed); + $this->ruleset_parsed = str_replace("RESET;",'$this->reset_();',$this->ruleset_parsed); + $this->ruleset_parsed = str_replace("UNLOCK_ALL;",'$this->unlock_all();',$this->ruleset_parsed); + $this->ruleset_parsed = str_replace("UNLOCK;",'$this->unlock();',$this->ruleset_parsed); + } \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/source/ValueCache/ValueCache_class.php b/code/web/app/app_achievements/_AchWebParser/source/ValueCache/ValueCache_class.php index 84c174124..7918e27e5 100644 --- a/code/web/app/app_achievements/_AchWebParser/source/ValueCache/ValueCache_class.php +++ b/code/web/app/app_achievements/_AchWebParser/source/ValueCache/ValueCache_class.php @@ -1,26 +1,19 @@ types[] = "c_cache"; - - $this->write = true; + parent::__construct(); } - function getData($type,$ident,$field) { + function getData($ident,$field,$type) { + $res = $DBc->sendSQL("SELECT apv_value,apv_date FROM ach_player_valuecache WHERE apv_name='".$DBc->mre($field)."' AND apv_player='".$DBc->mre($ident)."'","ARRAY"); + return array($res[0]['apv_value'],$res[0]['apv_date']); } - function writeData($type,$ident,$field = array(),$value = array()) { + function writeData($ident,$field,$data,$type) { global $DBc; - if($type == "c_cache") { - $DBc->sendSQL("INSERT INTO ach_player_valuecache () VALUES () ON DUPLICATE KEY UPDATE "); - - return true; - } - else { - return false; - } + $DBc->sendSQL("INSERT INTO ach_player_valuecache (apv_name,apv_player,apv_value,apv_date) VALUES ('".$DBc->mre($field)."','".$DBc->mre($ident)."','".$DBc->mre($data)."','".time()."') ON DUPLICATE KEY UPDATE apv_value='".$DBc->mre($data)."', apv_date='".time()."'","NONE"); } } ?> \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/source/ValueCache/conf.php b/code/web/app/app_achievements/_AchWebParser/source/ValueCache/conf.php new file mode 100644 index 000000000..117d56f0a --- /dev/null +++ b/code/web/app/app_achievements/_AchWebParser/source/ValueCache/conf.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/code/web/app/app_achievements/_AchWebParser/source/XMLapi/XMLapi_class.php b/code/web/app/app_achievements/_AchWebParser/source/XMLapi/XMLapi_class.php index 6ae24a6ac..e1b2270f0 100644 --- a/code/web/app/app_achievements/_AchWebParser/source/XMLapi/XMLapi_class.php +++ b/code/web/app/app_achievements/_AchWebParser/source/XMLapi/XMLapi_class.php @@ -1,17 +1,29 @@ types[] = "c_stats"; - $this->types[] = "c_items"; + parent::__construct(); - $this->write = false; + $this->xml_path = $CONF['xml_path']; } - function getData() { - + function getData($ident,$field,$type) { + switch($type) { + case "c_stats": + $path = $this->xml_path."full/".$ident.".xml"; + break; + case "c_items": + $path = $this->xml_path."item/".$ident.".xml"; + break; + default: + return false; + break; + } + $xml = new SimpleXMLElement($string); } - function writeData() { + function writeData($ident,$field,$data,$type) { return false; } diff --git a/code/web/app/app_achievements/_AchWebParser/source/XMLapi/conf.php b/code/web/app/app_achievements/_AchWebParser/source/XMLapi/conf.php new file mode 100644 index 000000000..d71249d09 --- /dev/null +++ b/code/web/app/app_achievements/_AchWebParser/source/XMLapi/conf.php @@ -0,0 +1,8 @@ + \ No newline at end of file