From 1a9d9dd9606e6ed3721a4a7e60691320e67c7a8b Mon Sep 17 00:00:00 2001 From: "vageyenaman@gmail.com" Date: Sat, 3 Mar 2012 07:00:31 +0000 Subject: [PATCH] Fixes for NTSL. NTSL now has a fixed statement-processing cap: scripts will crash when more than 1000 statements are called, and alert admins (scripts over 1000 statements are assumed to be buggy or malicious). You can now properly sleep without waking up every half a second. Work on footprints. There are now different kinds of footprints, and different blood makes different colored prints. Animals leave pawprints, humans leave footprints, aliens leave big claw prints. git-svn-id: http://tgstation13.googlecode.com/svn/trunk@3242 316c924e-a436-60f5-8080-3fe189b3f50e --- code/game/machinery/telecomms/broadcaster.dm | 3 + code/game/machinery/telecomms/logbrowser.dm | 11 +- .../machinery/telecomms/telecomunications.dm | 9 +- .../machinery/telecomms/traffic_control.dm | 9 +- code/modules/detectivework/detective_work.dm | 127 ++++++++++------ code/modules/mob/living/carbon/human/life.dm | 3 +- code/modules/scripting/Errors.dm | 6 +- .../scripting/Implementations/Telecomms.dm | 4 +- .../scripting/Interpreter/Interaction.dm | 2 + .../scripting/Interpreter/Interpreter.dm | 135 ++++++++++-------- icons/effects/footprints.dmi | Bin 742 -> 2391 bytes 11 files changed, 196 insertions(+), 113 deletions(-) diff --git a/code/game/machinery/telecomms/broadcaster.dm b/code/game/machinery/telecomms/broadcaster.dm index 7fc6064fcb..9e14ec3b5d 100644 --- a/code/game/machinery/telecomms/broadcaster.dm +++ b/code/game/machinery/telecomms/broadcaster.dm @@ -265,6 +265,9 @@ var if (R.client && R.client.STFU_radio) //Adminning with 80 people on can be fun when you're trying to talk and all you can hear is radios. continue + if(istype(M, /mob/new_player)) // we don't want new players to hear messages. rare but generates runtimes. + continue + // --- Check for compression --- if(compression > 0) diff --git a/code/game/machinery/telecomms/logbrowser.dm b/code/game/machinery/telecomms/logbrowser.dm index eb6d6368bb..c64b7bae7e 100644 --- a/code/game/machinery/telecomms/logbrowser.dm +++ b/code/game/machinery/telecomms/logbrowser.dm @@ -58,10 +58,12 @@ for(var/datum/comm_log_entry/C in SelectedServer.log_entries) i++ - dat += "
  • [C.name] \[X\]
    " // If the log is a speech file if(C.input_type == "Speech File") + + dat += "
  • [C.name] \[X\]
    " + // -- Determine race of orator -- var/race // The actual race of the mob @@ -113,9 +115,10 @@ dat += "

  • " else if(C.input_type == "Execution Error") - dat += "Data type: [C.input_type]
    " - dat += "Source: Internal server code
    " - dat += "Contents: \"[C.parameters["message"]]\"
    " + + dat += "
  • [C.name] \[X\]
    " + dat += "Output: \"[C.parameters["message"]]\"
    " + dat += "

  • " dat += "" diff --git a/code/game/machinery/telecomms/telecomunications.dm b/code/game/machinery/telecomms/telecomunications.dm index 05780256fe..6f1e45765a 100644 --- a/code/game/machinery/telecomms/telecomunications.dm +++ b/code/game/machinery/telecomms/telecomunications.dm @@ -403,7 +403,9 @@ log.parameters["message"] = signal.data["message"] log.parameters["name"] = signal.data["name"] log.parameters["realname"] = signal.data["realname"] - log.parameters["uspeech"] = M.universal_speak + + if(!istype(M, /mob/new_player)) + log.parameters["uspeech"] = M.universal_speak // If the signal is still compressed, make the log entry gibberish if(signal.data["compression"] > 0) @@ -453,9 +455,12 @@ proc/add_entry(var/content, var/input) var/datum/comm_log_entry/log = new var/identifier = num2text( rand(-1000,1000) + world.time ) - log.name = "data packet ([md5(identifier)])" + log.name = "[input] ([md5(identifier)])" log.input_type = input log.parameters["message"] = content + log_entries.Add(log) + update_logs() + diff --git a/code/game/machinery/telecomms/traffic_control.dm b/code/game/machinery/telecomms/traffic_control.dm index bb495decc3..753acf6e79 100644 --- a/code/game/machinery/telecomms/traffic_control.dm +++ b/code/game/machinery/telecomms/traffic_control.dm @@ -10,6 +10,7 @@ screen = 0 // the screen number: list/servers = list() // the servers located by the computer mob/editingcode + mob/lasteditor list/viewingcode = list() obj/machinery/telecomms/server/SelectedServer @@ -49,13 +50,12 @@ showcode = dd_replacetext(storedcode, "\"", "\\\"") for(var/mob/M in viewingcode) - if(M.machine == src && M in view(1, src)) + if( (M.machine == src && M in view(1, src) ) || issilicon(M)) winset(M, "tcscode", "is-disabled=true") winset(M, "tcscode", "text=\"[showcode]\"") else - if(!issilicon(M)) - viewingcode.Remove(M) - winshow(M, "Telecomms IDE", 0) // hide the window! + viewingcode.Remove(M) + winshow(M, "Telecomms IDE", 0) // hide the window! sleep(5) @@ -164,6 +164,7 @@ if(usr in viewingcode) return if(!editingcode) + lasteditor = usr editingcode = usr winshow(editingcode, "Telecomms IDE", 1) // show the IDE winset(editingcode, "tcscode", "is-disabled=false") diff --git a/code/modules/detectivework/detective_work.dm b/code/modules/detectivework/detective_work.dm index d298d6d505..dce59c2d0f 100644 --- a/code/modules/detectivework/detective_work.dm +++ b/code/modules/detectivework/detective_work.dm @@ -710,63 +710,89 @@ obj/machinery/computer/forensic_scanning obj/item/clothing/shoes/var track_blood = 0 mob/living/carbon/human/track_blood_mob + track_blood_type mob/var bloody_hands = 0 mob/living/carbon/human/bloody_hands_mob track_blood mob/living/carbon/human/track_blood_mob + track_blood_type obj/item/clothing/gloves/var transfer_blood = 0 mob/living/carbon/human/bloody_hands_mob -obj/effect/decal/cleanable/blood/var - track_amt = 2 +obj/effect/decal/cleanable/var + track_amt = 3 mob/blood_owner turf/Exited(mob/living/carbon/human/M) if(istype(M,/mob/living) && !istype(M,/mob/living/carbon/metroid)) - if(!istype(src, /turf/space)) // Bloody tracks code starts here - if(M.track_blood > 0) - M.track_blood-- - src.add_bloody_footprints(M.track_blood_mob,1,M.dir,get_tracks(M)) - else if(istype(M,/mob/living/carbon/human)) - if(M.shoes) - if(M.shoes.track_blood > 0) - M.shoes.track_blood-- - src.add_bloody_footprints(M.shoes.track_blood_mob,1,M.dir,M.shoes.name) // And bloody tracks end here + var/dofoot = 1 + if(istype(M,/mob/living/simple_animal)) + if(!(istype(M,/mob/living/simple_animal/cat) || istype(M,/mob/living/simple_animal/corgi) || istype(M,/mob/living/simple_animal/constructwraith))) + dofoot = 0 + + if(dofoot) + + if(!istype(src, /turf/space)) // Bloody tracks code starts here + if(M.track_blood > 0) + M.track_blood-- + src.add_bloody_footprints(M.track_blood_mob,1,M.dir,get_tracks(M),M.track_blood_type) + else if(istype(M,/mob/living/carbon/human)) + if(M.shoes) + if(M.shoes.track_blood > 0) + M.shoes.track_blood-- + src.add_bloody_footprints(M.shoes.track_blood_mob,1,M.dir,M.shoes.name,M.shoes.track_blood_type) // And bloody tracks end here . = ..() turf/Entered(mob/living/carbon/human/M) if(istype(M,/mob/living) && !istype(M,/mob/living/carbon/metroid)) - if(M.track_blood > 0) - M.track_blood-- - src.add_bloody_footprints(M.track_blood_mob,0,M.dir,get_tracks(M)) - else if(istype(M,/mob/living/carbon/human)) - if(M.shoes && !istype(src,/turf/space)) - if(M.shoes.track_blood > 0) - M.shoes.track_blood-- - src.add_bloody_footprints(M.shoes.track_blood_mob,0,M.dir,M.shoes.name) + var/dofoot = 1 + if(istype(M,/mob/living/simple_animal)) + if(!(istype(M,/mob/living/simple_animal/cat) || istype(M,/mob/living/simple_animal/corgi) || istype(M,/mob/living/simple_animal/constructwraith))) + dofoot = 0 + + if(dofoot) + + if(M.track_blood > 0) + M.track_blood-- + src.add_bloody_footprints(M.track_blood_mob,0,M.dir,get_tracks(M),M.track_blood_type) + else if(istype(M,/mob/living/carbon/human)) + if(M.shoes && !istype(src,/turf/space)) + if(M.shoes.track_blood > 0) + M.shoes.track_blood-- + src.add_bloody_footprints(M.shoes.track_blood_mob,0,M.dir,M.shoes.name,M.shoes.track_blood_type) - for(var/obj/effect/decal/cleanable/blood/B in src) - if(B.track_amt <= 0) continue - if(B.type != /obj/effect/decal/cleanable/blood/tracks) - if(istype(M,/mob/living/carbon/human)) - if(M.shoes) - M.shoes.add_blood(B.blood_owner) - M.shoes.track_blood_mob = B.blood_owner - M.shoes.track_blood = max(M.shoes.track_blood,8) - else - M.add_blood(B.blood_owner) - M.track_blood_mob = B.blood_owner - M.track_blood = max(M.track_blood,rand(4,8)) - B.track_amt-- - break + for(var/obj/effect/decal/cleanable/B in src) + if(B:track_amt <= 0) continue + if(B.type != /obj/effect/decal/cleanable/blood/tracks) + if(istype(B, /obj/effect/decal/cleanable/xenoblood) || istype(B, /obj/effect/decal/cleanable/blood) || istype(B, /obj/effect/decal/cleanable/oil) || istype(B, /obj/effect/decal/cleanable/robot_debris)) + + var/track_type = "blood" + if(istype(B, /obj/effect/decal/cleanable/xenoblood)) + track_type = "xeno" + else if(istype(B, /obj/effect/decal/cleanable/oil) || istype(B, /obj/effect/decal/cleanable/robot_debris)) + track_type = "oil" + + if(istype(M,/mob/living/carbon/human)) + if(M.shoes) + M.shoes.add_blood(B.blood_owner) + M.shoes.track_blood_mob = B.blood_owner + M.shoes.track_blood = max(M.shoes.track_blood,8) + M.shoes.track_blood_type = track_type + else + M.add_blood(B.blood_owner) + M.track_blood_mob = B.blood_owner + M.track_blood = max(M.track_blood,rand(4,8)) + M.track_blood_type = track_type + B.track_amt-- + break . = ..() -turf/proc/add_bloody_footprints(mob/living/carbon/human/M,leaving,d,info) +turf/proc/add_bloody_footprints(mob/living/carbon/human/M,leaving,d,info,bloodcolor) for(var/obj/effect/decal/cleanable/blood/tracks/T in src) - if(T.dir == d) + if(T.dir == d && findtext(T.icon, bloodcolor)) if((leaving && T.icon_state == "steps2") || (!leaving && T.icon_state == "steps1")) T.desc = "These bloody footprints appear to have been made by [info]." if(T.blood_DNA) @@ -780,12 +806,29 @@ turf/proc/add_bloody_footprints(mob/living/carbon/human/M,leaving,d,info) return var/obj/effect/decal/cleanable/blood/tracks/this = new(src) this.icon = 'footprints.dmi' + + var/preiconstate = "" + + if(info == "animal paws") + preiconstate = "paw" + else if(info == "alien claws") + preiconstate = "claw" + else if(info == "small alien feet") + preiconstate = "paw" + if(leaving) - this.icon_state = "blood2" + this.icon_state = "[bloodcolor][preiconstate]2" else - this.icon_state = "blood1" + this.icon_state = "[bloodcolor][preiconstate]1" this.dir = d - this.desc = "These bloody footprints appear to have been made by [info]." + + if(bloodcolor == "blood") + this.desc = "These bloody footprints appear to have been made by [info]." + else if(bloodcolor == "xeno") + this.desc = "These acidic bloody footprints appear to have been made by [info]." + else if(bloodcolor == "oil") + this.desc = "These oil footprints appear to have been made by [info]." + if(istype(M,/mob/living/carbon/human)) if(this.blood_DNA.len) this.blood_DNA.len++ @@ -797,12 +840,14 @@ proc/get_tracks(mob/M) if(istype(M,/mob/living)) if(istype(M,/mob/living/carbon/human)) . = "human feet" - else if(istype(M,/mob/living/carbon/monkey)) - . = "monkey paws" + else if(istype(M,/mob/living/carbon/monkey) || istype(M,/mob/living/simple_animal/cat) || istype(M,/mob/living/simple_animal/corgi) || istype(M,/mob/living/simple_animal/crab)) + . = "animal paws" else if(istype(M,/mob/living/silicon/robot)) . = "robot feet" - else if(istype(M,/mob/living/carbon/alien)) + else if(istype(M,/mob/living/carbon/alien/humanoid)) . = "alien claws" + else if(istype(M,/mob/living/carbon/alien/larva)) + . = "small alien feet" else . = "an unknown creature" diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index 74d9a0d6b6..b17f608c46 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -728,7 +728,8 @@ lying = 1 stat = 0 if (paralysis > 0) - handle_dreams() + if(sleeping > 0) + handle_dreams() AdjustParalysis(-1) blinded = 1 lying = 1 diff --git a/code/modules/scripting/Errors.dm b/code/modules/scripting/Errors.dm index 361d762048..bb92bf8f40 100644 --- a/code/modules/scripting/Errors.dm +++ b/code/modules/scripting/Errors.dm @@ -121,4 +121,8 @@ DivisionByZero name="DivideByZeroError" - message="Division by zero attempted." \ No newline at end of file + message="Division by zero attempted." + + MaxCPU + name="MaxComputationalUse" + message="Maximum amount of computational cycles reached (>= 1000)." \ No newline at end of file diff --git a/code/modules/scripting/Implementations/Telecomms.dm b/code/modules/scripting/Implementations/Telecomms.dm index 6ab82d9d0f..5801cf2eb4 100644 --- a/code/modules/scripting/Implementations/Telecomms.dm +++ b/code/modules/scripting/Implementations/Telecomms.dm @@ -47,6 +47,8 @@ if(!interpreter) return + interpreter.container = src + interpreter.SetVar("PI" , 3.141592653) // value of pi interpreter.SetVar("E" , 2.718281828) // value of e interpreter.SetVar("SQURT2" , 1.414213562) // value of the square root of 2 @@ -137,7 +139,7 @@ @param container: the list or container to measure */ - interpreter.SetProc("length", /proc/smartfind) + interpreter.SetProc("length", /proc/smartlength) /* -- Clone functions, carried from default BYOND procs --- */ diff --git a/code/modules/scripting/Interpreter/Interaction.dm b/code/modules/scripting/Interpreter/Interaction.dm index 0dda1656d6..f0b7c988d2 100644 --- a/code/modules/scripting/Interpreter/Interaction.dm +++ b/code/modules/scripting/Interpreter/Interaction.dm @@ -28,6 +28,8 @@ */ Run() cur_recursion = 0 // reset recursion + cur_statements = 0 // reset CPU tracking + alertadmins = 0 ASSERT(src.program) RunBlock(src.program) diff --git a/code/modules/scripting/Interpreter/Interpreter.dm b/code/modules/scripting/Interpreter/Interpreter.dm index e9860176f3..70849ffa15 100644 --- a/code/modules/scripting/Interpreter/Interpreter.dm +++ b/code/modules/scripting/Interpreter/Interpreter.dm @@ -24,6 +24,8 @@ stack scopes = new() functions = new() + + datum/container // associated container for interpeter /* Var: status A variable indicating that the rest of the current block should be skipped. This may be set to any combination of . @@ -31,10 +33,12 @@ status=0 returnVal - max_iterations=100 // max iterations without any kind of delay - cur_iterations=0 // current iteration - max_recursion=50 // max recursions without returning anything (or completing the code block) - cur_recursion=0 // current amount of recursion + max_statements=1000 // maximum amount of statements that can be called in one execution. this is to prevent massive crashes and exploitation + cur_statements=0 // current amount of statements called + alertadmins=0 // set to 1 if the admins shouldn't be notified of anymore issues + max_iterations=100 // max number of uninterrupted loops possible + max_recursion=50 // max recursions without returning anything (or completing the code block) + cur_recursion=0 // current amount of recursion /* Var: persist If 0, global variables will be reset after Run() finishes. @@ -88,56 +92,74 @@ CreateGlobalScope() curScope = globalScope - for(var/node/statement/S in Block.statements) - while(paused) sleep(10) - if(istype(S, /node/statement/VariableAssignment)) - var/node/statement/VariableAssignment/stmt = S - var/name = stmt.var_name.id_name - if(!stmt.object) - // Below we assign the variable first to null if it doesn't already exist. - // This is necessary for assignments like +=, and when the variable is used in a function - // If the variable already exists in a different block, then AssignVariable will automatically use that one. - if(!IsVariableAccessible(name)) - AssignVariable(name, null) - AssignVariable(name, Eval(stmt.value)) + if(cur_statements < max_statements) + + for(var/node/statement/S in Block.statements) + while(paused) sleep(10) + + cur_statements++ + if(cur_statements >= max_statements) + RaiseError(new/runtimeError/MaxCPU()) + + if(container && !alertadmins) + if(istype(container, /datum/TCS_Compiler)) + var/datum/TCS_Compiler/Compiler = container + var/obj/machinery/telecomms/server/Holder = Compiler.Holder + var/message = "Potential crash-inducing NTSL script detected at telecommunications server [Compiler.Holder] ([Holder.x], [Holder.y], [Holder.z])." + + alertadmins = 1 + message_admins(message, 1) + break + + if(istype(S, /node/statement/VariableAssignment)) + var/node/statement/VariableAssignment/stmt = S + var/name = stmt.var_name.id_name + if(!stmt.object) + // Below we assign the variable first to null if it doesn't already exist. + // This is necessary for assignments like +=, and when the variable is used in a function + // If the variable already exists in a different block, then AssignVariable will automatically use that one. + if(!IsVariableAccessible(name)) + AssignVariable(name, null) + AssignVariable(name, Eval(stmt.value)) + else + var/datum/D = Eval(GetVariable(stmt.object.id_name)) + if(!D) return + D.vars[stmt.var_name.id_name] = Eval(stmt.value) + else if(istype(S, /node/statement/VariableDeclaration)) + //VariableDeclaration nodes are used to forcibly declare a local variable so that one in a higher scope isn't used by default. + var/node/statement/VariableDeclaration/dec=S + if(!dec.object) + AssignVariable(dec.var_name.id_name, null, curScope) + else + var/datum/D = Eval(GetVariable(dec.object.id_name)) + if(!D) return + D.vars[dec.var_name.id_name] = null + else if(istype(S, /node/statement/FunctionCall)) + RunFunction(S) + else if(istype(S, /node/statement/FunctionDefinition)) + //do nothing + else if(istype(S, /node/statement/WhileLoop)) + RunWhile(S) + else if(istype(S, /node/statement/IfStatement)) + RunIf(S) + else if(istype(S, /node/statement/ReturnStatement)) + if(!curFunction) + RaiseError(new/runtimeError/UnexpectedReturn()) + continue + status |= RETURNING + returnVal=Eval(S:value) + break + else if(istype(S, /node/statement/BreakStatement)) + status |= BREAKING + break + else if(istype(S, /node/statement/ContinueStatement)) + status |= CONTINUING + break else - var/datum/D = Eval(GetVariable(stmt.object.id_name)) - if(!D) return - D.vars[stmt.var_name.id_name] = Eval(stmt.value) - else if(istype(S, /node/statement/VariableDeclaration)) - //VariableDeclaration nodes are used to forcibly declare a local variable so that one in a higher scope isn't used by default. - var/node/statement/VariableDeclaration/dec=S - if(!dec.object) - AssignVariable(dec.var_name.id_name, null, curScope) - else - var/datum/D = Eval(GetVariable(dec.object.id_name)) - if(!D) return - D.vars[dec.var_name.id_name] = null - else if(istype(S, /node/statement/FunctionCall)) - RunFunction(S) - else if(istype(S, /node/statement/FunctionDefinition)) - //do nothing - else if(istype(S, /node/statement/WhileLoop)) - RunWhile(S) - else if(istype(S, /node/statement/IfStatement)) - RunIf(S) - else if(istype(S, /node/statement/ReturnStatement)) - if(!curFunction) - RaiseError(new/runtimeError/UnexpectedReturn()) - continue - status |= RETURNING - returnVal=Eval(S:value) - break - else if(istype(S, /node/statement/BreakStatement)) - status |= BREAKING - break - else if(istype(S, /node/statement/ContinueStatement)) - status |= CONTINUING - break - else - RaiseError(new/runtimeError/UnknownInstruction()) - if(status) - break + RaiseError(new/runtimeError/UnknownInstruction()) + if(status) + break + curScope = scopes.Pop() /* @@ -212,14 +234,9 @@ */ RunWhile(node/statement/WhileLoop/stmt) var/i=1 - if(!cur_iterations) - cur_iterations = 1 while(Eval(stmt.cond) && Iterate(stmt.block, i++)) - cur_iterations++ continue status &= ~BREAKING - cur_iterations -= i - if(cur_iterations <= 0) cur_iterations = 0 /* Proc:Iterate @@ -227,7 +244,7 @@ */ Iterate(node/BlockDefinition/block, count) RunBlock(block) - if(max_iterations > 0 && (count >= max_iterations || cur_iterations + 1 >= max_iterations)) + if(max_iterations > 0 && count >= max_iterations) RaiseError(new/runtimeError/IterationLimitReached()) return 0 if(status & (BREAKING|RETURNING)) diff --git a/icons/effects/footprints.dmi b/icons/effects/footprints.dmi index 091b6398c4c4f8e84fc98b3afe0af41f382a44ce..ede9c8e17d65338ff60e108aeefb62c6e011c179 100644 GIT binary patch literal 2391 zcmV-d38?moP)XL9LW0ErvHko)TNe|<-QG(MC!FDx?A%_uo;BBwex&o- z4(!B^yFDeN7D2`3P*b9$eyXr8KJXypSlo=YqADm6+WGW~l856jnda&TtFzc72b-5A zD(|w|Z0%v<+UPYMgfHz5ZbyatD+1(oERu+b&`s=T&zOFK3Cd;C2Ts`WbZMpYfpVk3~ zSfI`E|6(LfXc!H)w80p~#z-;~qj<|h+---^E1oYG^v?K|`ry6d$Q=5?2RuJmchIl% z!F$t_mr*)UG9#Q&U}OWO$4CH@P0E@OqjWPdQue4YjY(T2c>)E*L(M0!LoERDO}@O2d&Er2xtXLt~ zGtzi>&-4zqD(Elmp$4<$b4IotZALeMI3o#Y>^j{E z)Nhqhd-QgHjDjUM{y8kMc8eA+aKRSbTSUo?e8w1xQ3O!zQ%^A1u|UDrJs6|N7zKgE zAWq}(!DM|T0?CZ(RU(kI_EbhvTGy#KQZ|MHDzZ8eM+$Z-qi;xm7-*5vJlQ}M5)^f` z%KUdsuybe;y{#=q6W`#U?LkQpe)&@l4CP?IwTBPLFnX$95XedYd+`qka*P6z%Qhnl z1U6bM#9Ftgs!F;CHd(D!Hy!dkA4&HnlL;UEN7aYO`*-x8Ig=F&`k(dNZJ?*>sp3E? z=i0kN1plm->)}qrNYTe(S{vePT#ChZd+GKqs0?CXT&vVF4 zAZhKXjHI+~Q*oqh3{LeIkp9PjY9uJ?=sEM>F~KgZ7O}M$&4j@gi4~p~ z>jD_c!TxBEn~c`=I|4cB|1AGa0wR}fMieN91h zCMy>7zZw-HlT{aJU9W2nq=Nq09wLxiJ~|)|$(|7f0yBd3Ep5ej-qzhJpiQ{Wf>E@G z&$^js?crnU1(HnIyZ{vcK<5WS<;OCT_8eZ1d!Um`uBf9#MzW4lfj~!MV-#n0CJ=QL zn%M=?pWmPUkRb19$+p3_VObw}l>{U+8b^Ke-tWPUl?Sk8)JtBkFJ?Zelg0rp4;7qB zj1C9khGk>)5`glTpOycP03-!F|M7zWiGOs=Apnu-U*f5g%su!?jP?{LCxLf!;0J1e zU~c(SO)V)?8UhpSlD3Gkm9ymmksP67QmBr~ZMh8qjonvajZ6%qZS#phL})e(24sXw zf$+AGe*e)%%m~2UgWetR+%%0Izo$TL`i%gVmAVAkVw<}xOWKbCM*)s8i?3P$0N^b* zAEi-)c7rA2yn8JGaZ_)bZ9{-yAv)+>05^g7RgeJ}z|HFL%yu3CF#*Wcp==Ls5M;V# zdQiJML>O#dvj0`EBbx)^qy~_R^hzg)$KgxpSC;(#uMpI6R7JPUpXNJ&{KB)V!vjF1 zF~*v|bz`fJHX+SJy@Nj-&@t#y)B^!c!!8}pG3d65ddchbJ`m96T!&oF=a5Hd)1X)J z{uZ1|0J0TP{Q5vTZ(8yaX=A`(KUbTj%d}c3123X zaU>fIDEyjJ*$f0Uk9wu*atXTaqF(a)`Wy&ocWpv0*K5e5vw6^~{QL;c6$kYDHvtlV z!YVNUG+6X|l$HJ+o;tz3fM56G9R3Nq<_^xhnVSRf+co5UPONvOH|qx%(=tmPrqE1yv!CEi}S{0fBIPLS~d%nvs70(XQPj z#NC4c)W+^&mR6iV0A@3)j~xnN*QiU7EdVYr?eV4Oo7P4EfTIBCn2#?l00RK4x$K&G zqXjTU0bJ}Q;({((0DMzl+x2?{pckUU#|6NEBmw9_R{(Scz^a2J02_!2z*8Lr0qAMI z>`wz~R|mzq$uc4U=PqpCoio7G9J>8)CglI8rU#IU^hGDICMElyJ=uAd(63CW=<<*F zLdgx6SZ4kva0igTEk(4UIG~8e=$gNEN3?3!gfA1xIFj82`VWTFz~c=go(}*3002ov JPDHLkV1jStReS&d literal 742 zcmV005u_0{{R3dEt5<0000FP)t-sz`($& z1_l7k00000z`($d4fkIF0004WQchCV=-0C=2J zR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+(=$pSoZ^zil2jm5 zDJdsEKgCdqi!&v&s2HS+i!-e#F*g;&HbhfqL{M2pYF<8J!$7JCo0XrLL)a*wBAg~E zE4cc(fCB{p`GZb)`%24q0005?NkleK%&1&p8J75d+U-ou#3<7cS5yK!V~|!I5B`khHnl! zLziOpKe$ZMC=Nu`3`|9>aA>MQjl*_8c!nxnprLvce;A5Cp~la!N@d3Krc$uL_zvR( z;TcwooM9$r+cUj|sZ4)A$SJEF?|aFZd|O$cA%ToTAF#$DnIQudGbF_UUE)r;T3wq3 zJD5{IS5Y%SUk!e&ew%^-O%62~?oQcN)Tanu=C^#fI0jRFoNGK4#<%P>~|gR4t3Fdc}(p{Zmv4&wvi83xc74ny%L)TkN$ Y0C479aj`FDyZ`_I07*qoM6N<$g3=U1r~m)}