Merge branch 'master' into upstream-merge-29839

This commit is contained in:
LetterJay
2017-09-11 18:03:51 -05:00
committed by GitHub
92 changed files with 2479 additions and 2105 deletions

View File

@@ -5,7 +5,7 @@
// How multiple components of the exact same type are handled in the same datum
#define COMPONENT_DUPE_HIGHLANDER 0 //old component is deleted (default)
#define COMPONENT_DUPE_ALLOWED 1 //duplicates allowed
#define COMPONENT_DUPE_ALLOWED 1 //duplicates allowed
#define COMPONENT_DUPE_UNIQUE 2 //new component is deleted
// All signals. Format:
@@ -16,3 +16,9 @@
#define COMSIG_PARENT_QDELETED "parent_qdeleted" //before a datum's Destroy() is called: ()
#define COMSIG_ATOM_ENTERED "atom_entered" //from base of atom/Entered(): (atom/movable, atom)
#define COMSIG_MOVABLE_CROSSED "movable_crossed" //from base of atom/movable/Crossed(): (atom/movable)
#define COMSIG_PARENT_ATTACKBY "atom_attackby" //from the base of atom/attackby: (obj/item, mob/living, params)
#define COMSIG_PARENT_EXAMINE "atom_examine" //from the base of atom/examine: (mob)
#define COMSIG_ATOM_ENTERED "atom_entered" //from base of atom/Entered(): (atom/movable, atom)
#define COMSIG_ATOM_EX_ACT "atom_ex_act" //from base of atom/ex_act(): (severity, target)
#define COMSIG_ATOM_SING_PULL "atom_sing_pull" //from base of atom/singularity_pull(): (S, current_size)
#define COMSIG_MOVABLE_CROSSED "movable_crossed" //from base of atom/movable/Crossed(): (atom/movable)

View File

@@ -25,6 +25,8 @@
#define islava(A) (istype(A, /turf/open/lava))
#define isplatingturf(A) (istype(A, /turf/open/floor/plating))
//Mobs
#define isliving(A) (istype(A, /mob/living))

View File

@@ -453,3 +453,13 @@ GLOBAL_LIST_INIT(ghost_others_options, list(GHOST_OTHERS_SIMPLE, GHOST_OTHERS_DE
#define MOUSE_OPACITY_TRANSPARENT 0
#define MOUSE_OPACITY_ICON 1
#define MOUSE_OPACITY_OPAQUE 2
//world/proc/shelleo
#define SHELLEO_ERRORLEVEL 1
#define SHELLEO_STDOUT 2
#define SHELLEO_STDERR 3
//server security mode
#define SECURITY_SAFE 1
#define SECURITY_ULTRASAFE 2
#define SECURITY_TRUSTED 3

View File

@@ -9,7 +9,9 @@
//keep these in sync with TGS3
#define SERVICE_WORLD_PARAM "server_service"
#define SERVICE_PR_TEST_JSON "..\\..\\prtestjob.json"
#define SERVICE_VERSION_PARAM "server_service_version"
#define SERVICE_PR_TEST_JSON "prtestjob.json"
#define SERVICE_PR_TEST_JSON_OLD "..\\..\\[SERVICE_PR_TEST_JSON]"
#define SERVICE_CMD_HARD_REBOOT "hard_reboot"
#define SERVICE_CMD_GRACEFUL_SHUTDOWN "graceful_shutdown"

57
code/__HELPERS/shell.dm Normal file
View File

@@ -0,0 +1,57 @@
//Runs the command in the system's shell, returns a list of (error code, stdout, stderr)
#define SHELLEO_NAME "data/shelleo."
#define SHELLEO_ERR ".err"
#define SHELLEO_OUT ".out"
/world/proc/shelleo(command)
var/static/list/shelleo_ids = list()
var/stdout = ""
var/stderr = ""
var/errorcode = 1
var/shelleo_id
var/out_file = ""
var/err_file = ""
var/static/list/interpreters = list("[MS_WINDOWS]" = "cmd /c", "[UNIX]" = "sh -c")
var/interpreter = interpreters["[world.system_type]"]
if(interpreter)
for(var/seo_id in shelleo_ids)
if(!shelleo_ids[seo_id])
shelleo_ids[seo_id] = TRUE
shelleo_id = "[seo_id]"
break
if(!shelleo_id)
shelleo_id = "[shelleo_ids.len + 1]"
shelleo_ids += shelleo_id
shelleo_ids[shelleo_id] = TRUE
out_file = "[SHELLEO_NAME][shelleo_id][SHELLEO_OUT]"
err_file = "[SHELLEO_NAME][shelleo_id][SHELLEO_ERR]"
errorcode = shell("[interpreter] \"[command]\" > [out_file] 2> [err_file]")
if(fexists(out_file))
stdout = file2text(out_file)
fdel(out_file)
if(fexists(err_file))
stderr = file2text(err_file)
fdel(err_file)
shelleo_ids[shelleo_id] = FALSE
else
CRASH("Operating System: [world.system_type] not supported") // If you encounter this error, you are encouraged to update this proc with support for the new operating system
. = list(errorcode, stdout, stderr)
#undef SHELLEO_NAME
#undef SHELLEO_ERR
#undef SHELLEO_OUT
/proc/shell_url_scrub(url)
var/static/regex/bad_chars_regex = regex("\[^#%&./:=?\\w]*", "g")
var/scrubbed_url = ""
var/bad_match = ""
var/last_good = 1
var/bad_chars = 1
do
bad_chars = bad_chars_regex.Find(url)
scrubbed_url += copytext(url, last_good, bad_chars)
if(bad_chars)
bad_match = url_encode(bad_chars_regex.match)
scrubbed_url += bad_match
last_good = bad_chars + length(bad_match)
while(bad_chars)
. = scrubbed_url

View File

@@ -16,7 +16,7 @@ GLOBAL_PROTECT(config_dir)
return ..()
/datum/configuration/vv_edit_var(var_name, var_value)
var/static/list/banned_edits = list("cross_address", "cross_allowed", "autoadmin", "autoadmin_rank")
var/static/list/banned_edits = list("cross_address", "cross_allowed", "autoadmin", "autoadmin_rank", "invoke_youtubedl")
if(var_name in banned_edits)
return FALSE
return ..()
@@ -94,6 +94,8 @@ GLOBAL_PROTECT(config_dir)
var/panic_server_name
var/panic_address //Reconnect a player this linked server if this server isn't accepting new players
var/invoke_youtubedl
//IP Intel vars
var/ipintel_email
var/ipintel_rating_bad = 1
@@ -478,6 +480,8 @@ GLOBAL_PROTECT(config_dir)
if("panic_server_address")
if(value != "byond://address:port")
panic_address = value
if("invoke_youtubedl")
invoke_youtubedl = value
if("show_irc_name")
showircname = 1
if("see_own_notes")

View File

@@ -0,0 +1,92 @@
/datum/component/archaeology
dupe_type = COMPONENT_DUPE_UNIQUE
var/list/archdrops
var/prob2drop
var/dug
/datum/component/archaeology/Initialize(_prob2drop, list/_archdrops = list())
prob2drop = Clamp(_prob2drop, 0, 100)
archdrops = _archdrops
RegisterSignal(COMSIG_PARENT_ATTACKBY,.proc/Dig)
RegisterSignal(COMSIG_ATOM_EX_ACT, .proc/BombDig)
RegisterSignal(COMSIG_ATOM_SING_PULL, .proc/SingDig)
/datum/component/archaeology/InheritComponent(datum/component/archaeology/A, i_am_original)
var/list/other_archdrops = A.archdrops
var/list/_archdrops = archdrops
for(var/I in other_archdrops)
_archdrops[I] += other_archdrops[I]
/datum/component/archaeology/proc/Dig(mob/user, obj/item/W)
if(dug)
to_chat(user, "<span class='notice'>Looks like someone has dug here already.</span>")
return FALSE
else
var/digging_speed
if (istype(W, /obj/item/shovel))
var/obj/item/shovel/S = W
digging_speed = S.digspeed
else if (istype(W, /obj/item/pickaxe))
var/obj/item/pickaxe/P = W
digging_speed = P.digspeed
if (digging_speed && isturf(user.loc))
to_chat(user, "<span class='notice'>You start digging...</span>")
playsound(parent, 'sound/effects/shovel_dig.ogg', 50, 1)
if(do_after(user, digging_speed, target = parent))
to_chat(user, "<span class='notice'>You dig a hole.</span>")
gets_dug()
dug = TRUE
SSblackbox.add_details("pick_used_mining",W.type)
return TRUE
return FALSE
/datum/component/archaeology/proc/gets_dug()
if(dug)
return
else
var/turf/open/OT = get_turf(parent)
for(var/thing in archdrops)
var/maxtodrop = archdrops[thing]
for(var/i in 1 to maxtodrop)
if(prob(prob2drop)) // can't win them all!
new thing(OT)
if(isopenturf(OT))
if(OT.postdig_icon_change)
if(istype(OT, /turf/open/floor/plating/asteroid/) && !OT.postdig_icon)
var/turf/open/floor/plating/asteroid/AOT = parent
AOT.icon_plating = "[AOT.environment_type]_dug"
AOT.icon_state = "[AOT.environment_type]_dug"
else
if(isplatingturf(OT))
var/turf/open/floor/plating/POT = parent
POT.icon_plating = "[POT.postdig_icon]"
OT.icon_state = "[OT.postdig_icon]"
if(OT.slowdown) //Things like snow slow you down until you dig them.
OT.slowdown = 0
dug = TRUE
/datum/component/archaeology/proc/SingDig(S, current_size)
switch(current_size)
if(STAGE_THREE)
if(prob(30))
gets_dug()
if(STAGE_FOUR)
if(prob(50))
gets_dug()
else
if(current_size >= STAGE_FIVE && prob(70))
gets_dug()
/datum/component/archaeology/proc/BombDig(severity, target)
switch(severity)
if(3)
return
if(2)
if(prob(20))
gets_dug()
if(1)
gets_dug()

View File

@@ -22,46 +22,6 @@
//please call . = ..() first and append to the result, that way parent items are always at the top and child items are further down
//add separaters by doing . += "---"
/datum/proc/vv_get_dropdown()
. = list()
. += "---"
.["Call Proc"] = "?_src_=vars;proc_call=\ref[src]"
.["Mark Object"] = "?_src_=vars;mark_object=\ref[src]"
.["Delete"] = "?_src_=vars;delete=\ref[src]"
/datum/proc/on_reagent_change()
return
/client/proc/debug_variables(datum/D in world)
set category = "Debug"
set name = "View Variables"
//set src in world
var/static/cookieoffset = rand(1, 9999) //to force cookies to reset after the round.
if(!usr.client || !usr.client.holder)
to_chat(usr, "<span class='danger'>You need to be an administrator to access this.</span>")
return
if(!D)
return
var/islist = islist(D)
if (!islist && !istype(D))
return
var/title = ""
var/refid = "\ref[D]"
var/icon/sprite
var/hash
var/type = /list
if (!islist)
type = D.type
/datum/proc/vv_get_dropdown()
. = list()
. += "---"
@@ -103,14 +63,13 @@
>>>>>>> 84b1e3d... [s] Adds a security token to all admin hrefs (#29839)
if(istype(D, /atom))
var/atom/AT = D
if(AT.icon && AT.icon_state)
sprite = new /icon(AT.icon, AT.icon_state)
hash = md5(AT.icon)
hash = md5(hash + AT.icon_state)
usr << browse_rsc(sprite, "vv[hash].png")
src << browse_rsc(sprite, "vv[hash].png")
title = "[D] (\ref[D]) = [type]"
@@ -120,413 +79,6 @@
var/list/atomsnowflake = list()
if(istype(D, /atom))
var/atom/A = D
if(isliving(A))
atomsnowflake += "<a href='?_src_=vars;rename=[refid]'><b>[D]</b></a>"
if(A.dir)
atomsnowflake += "<br><font size='1'><a href='?_src_=vars;rotatedatum=[refid];rotatedir=left'><<</a> <a href='?_src_=vars;datumedit=[refid];varnameedit=dir'>[dir2text(A.dir)]</a> <a href='?_src_=vars;rotatedatum=[refid];rotatedir=right'>>></a></font>"
var/mob/living/M = A
atomsnowflake += {"
<br><font size='1'><a href='?_src_=vars;datumedit=[refid];varnameedit=ckey'>[M.ckey ? M.ckey : "No ckey"]</a> / <a href='?_src_=vars;datumedit=[refid];varnameedit=real_name'>[M.real_name ? M.real_name : "No real name"]</a></font>
<br><font size='1'>
BRUTE:<font size='1'><a href='?_src_=vars;mobToDamage=[refid];adjustDamage=brute'>[M.getBruteLoss()]</a>
FIRE:<font size='1'><a href='?_src_=vars;mobToDamage=[refid];adjustDamage=fire'>[M.getFireLoss()]</a>
TOXIN:<font size='1'><a href='?_src_=vars;mobToDamage=[refid];adjustDamage=toxin'>[M.getToxLoss()]</a>
OXY:<font size='1'><a href='?_src_=vars;mobToDamage=[refid];adjustDamage=oxygen'>[M.getOxyLoss()]</a>
CLONE:<font size='1'><a href='?_src_=vars;mobToDamage=[refid];adjustDamage=clone'>[M.getCloneLoss()]</a>
BRAIN:<font size='1'><a href='?_src_=vars;mobToDamage=[refid];adjustDamage=brain'>[M.getBrainLoss()]</a>
STAMINA:<font size='1'><a href='?_src_=vars;mobToDamage=[refid];adjustDamage=stamina'>[M.getStaminaLoss()]</a>
</font>
"}
else
atomsnowflake += "<a href='?_src_=vars;datumedit=[refid];varnameedit=name'><b>[D]</b></a>"
if(A.dir)
atomsnowflake += "<br><font size='1'><a href='?_src_=vars;rotatedatum=[refid];rotatedir=left'><<</a> <a href='?_src_=vars;datumedit=[refid];varnameedit=dir'>[dir2text(A.dir)]</a> <a href='?_src_=vars;rotatedatum=[refid];rotatedir=right'>>></a></font>"
else
atomsnowflake += "<b>[D]</b>"
var/formatted_type = "[type]"
if(length(formatted_type) > 25)
var/middle_point = length(formatted_type) / 2
var/splitpoint = findtext(formatted_type,"/",middle_point)
if(splitpoint)
formatted_type = "[copytext(formatted_type,1,splitpoint)]<br>[copytext(formatted_type,splitpoint)]"
else
formatted_type = "Type too long" //No suitable splitpoint (/) found.
var/marked
if(holder.marked_datum && holder.marked_datum == D)
marked = "<br><font size='1' color='red'><b>Marked Object</b></font>"
var/varedited_line = ""
if(!islist && D.var_edited)
varedited_line = "<br><font size='1' color='red'><b>Var Edited</b></font>"
var/list/dropdownoptions = list()
if (islist)
dropdownoptions = list(
"---",
"Add Item" = "?_src_=vars;listadd=[refid]",
"Remove Nulls" = "?_src_=vars;listnulls=[refid]",
"Remove Dupes" = "?_src_=vars;listdupes=[refid]",
"Set len" = "?_src_=vars;listlen=[refid]",
"Shuffle" = "?_src_=vars;listshuffle=[refid]"
)
else
dropdownoptions = D.vv_get_dropdown()
var/list/dropdownoptions_html = list()
for (var/name in dropdownoptions)
var/link = dropdownoptions[name]
if (link)
dropdownoptions_html += "<option value='[link]'>[name]</option>"
else
dropdownoptions_html += "<option value>[name]</option>"
var/list/names = list()
if (!islist)
for (var/V in D.vars)
names += V
sleep(1)//For some reason, without this sleep, VVing will cause client to disconnect on certain objects.
var/list/variable_html = list()
if (islist)
var/list/L = D
for (var/i in 1 to L.len)
var/key = L[i]
var/value
if (IS_NORMAL_LIST(L) && !isnum(key))
value = L[key]
variable_html += debug_variable(i, value, 0, D)
else
names = sortList(names)
for (var/V in names)
if(D.can_vv_get(V))
variable_html += D.vv_get_var(V)
var/html = {"
<html>
<head>
<title>[title]</title>
<style>
body {
font-family: Verdana, sans-serif;
font-size: 9pt;
}
.value {
font-family: "Courier New", monospace;
font-size: 8pt;
}
</style>
</head>
<body onload='selectTextField(); updateSearch()' onkeydown='return checkreload()' onkeyup='updateSearch()'>
<script type="text/javascript">
function checkreload() {
if(event.keyCode == 116){ //F5 (to refresh properly)
document.getElementById("refresh_link").click();
event.preventDefault ? event.preventDefault() : (event.returnValue = false)
return false;
}
return true;
}
function updateSearch(){
var filter_text = document.getElementById('filter');
var filter = filter_text.value.toLowerCase();
if(event.keyCode == 13){ //Enter / return
var vars_ol = document.getElementById('vars');
var lis = vars_ol.getElementsByTagName("li");
for ( var i = 0; i < lis.length; ++i )
{
try{
var li = lis\[i\];
if ( li.style.backgroundColor == "#ffee88" )
{
alist = lis\[i\].getElementsByTagName("a")
if(alist.length > 0){
location.href=alist\[0\].href;
}
}
}catch(err) { }
}
return
}
if(event.keyCode == 38){ //Up arrow
var vars_ol = document.getElementById('vars');
var lis = vars_ol.getElementsByTagName("li");
for ( var i = 0; i < lis.length; ++i )
{
try{
var li = lis\[i\];
if ( li.style.backgroundColor == "#ffee88" )
{
if( (i-1) >= 0){
var li_new = lis\[i-1\];
li.style.backgroundColor = "white";
li_new.style.backgroundColor = "#ffee88";
return
}
}
}catch(err) { }
}
return
}
if(event.keyCode == 40){ //Down arrow
var vars_ol = document.getElementById('vars');
var lis = vars_ol.getElementsByTagName("li");
for ( var i = 0; i < lis.length; ++i )
{
try{
var li = lis\[i\];
if ( li.style.backgroundColor == "#ffee88" )
{
if( (i+1) < lis.length){
var li_new = lis\[i+1\];
li.style.backgroundColor = "white";
li_new.style.backgroundColor = "#ffee88";
return
}
}
}catch(err) { }
}
return
}
//This part here resets everything to how it was at the start so the filter is applied to the complete list. Screw efficiency, it's client-side anyway and it only looks through 200 or so variables at maximum anyway (mobs).
if(complete_list != null && complete_list != ""){
var vars_ol1 = document.getElementById("vars");
vars_ol1.innerHTML = complete_list
}
document.cookie="[refid][cookieoffset]search="+encodeURIComponent(filter);
if(filter == ""){
return;
}else{
var vars_ol = document.getElementById('vars');
var lis = vars_ol.getElementsByTagName("li");
for ( var i = 0; i < lis.length; ++i )
{
try{
var li = lis\[i\];
if ( li.innerText.toLowerCase().indexOf(filter) == -1 )
{
vars_ol.removeChild(li);
i--;
}
}catch(err) { }
}
}
var lis_new = vars_ol.getElementsByTagName("li");
for ( var j = 0; j < lis_new.length; ++j )
{
var li1 = lis\[j\];
if (j == 0){
li1.style.backgroundColor = "#ffee88";
}else{
li1.style.backgroundColor = "white";
}
}
}
function selectTextField() {
var filter_text = document.getElementById('filter');
filter_text.focus();
filter_text.select();
var lastsearch = getCookie("[refid][cookieoffset]search");
if (lastsearch) {
filter_text.value = lastsearch;
updateSearch();
}
}
function loadPage(list) {
if(list.options\[list.selectedIndex\].value == ""){
return;
}
location.href=list.options\[list.selectedIndex\].value;
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for(var i=0; i<ca.length; i++) {
var c = ca\[i\];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(name)==0) return c.substring(name.length,c.length);
}
return "";
}
</script>
<div align='center'>
<table width='100%'>
<tr>
<td width='50%'>
<table align='center' width='100%'>
<tr>
<td>
[sprite_text]
<div align='center'>
[atomsnowflake.Join()]
</div>
</td>
</tr>
</table>
<div align='center'>
<b><font size='1'>[formatted_type]</font></b>
[marked]
[varedited_line]
</div>
</td>
<td width='50%'>
<div align='center'>
<a id='refresh_link' href='?_src_=vars;datumrefresh=[refid]'>Refresh</a>
<form>
<select name="file" size="1"
onchange="loadPage(this.form.elements\[0\])"
target="_parent._top"
onmouseclick="this.focus()"
style="background-color:#ffffff">
<option value selected>Select option</option>
[dropdownoptions_html.Join()]
</select>
</form>
</div>
</td>
</tr>
</table>
</div>
<hr>
<font size='1'>
<b>E</b> - Edit, tries to determine the variable type by itself.<br>
<b>C</b> - Change, asks you for the var type first.<br>
<b>M</b> - Mass modify: changes this variable for all objects of this type.<br>
</font>
<hr>
<table width='100%'>
<tr>
<td width='20%'>
<div align='center'>
<b>Search:</b>
</div>
</td>
<td width='80%'>
<input type='text' id='filter' name='filter_text' value='' style='width:100%;'>
</td>
</tr>
</table>
<hr>
<ol id='vars'>
[variable_html.Join()]
</ol>
<script type='text/javascript'>
var vars_ol = document.getElementById("vars");
var complete_list = vars_ol.innerHTML;
</script>
</body>
</html>
"}
usr << browse(html, "window=variables[refid];size=475x650")
#define VV_HTML_ENCODE(thing) ( sanitize ? html_encode(thing) : thing )
/proc/debug_variable(name, value, level, datum/DA = null, sanitize = TRUE)
var/header
if(DA)
if (islist(DA))
var/index = name
if (value)
name = DA[name] //name is really the index until this line
else
value = DA[name]
header = "<li style='backgroundColor:white'>(<a href='?_src_=vars;listedit=\ref[DA];index=[index]'>E</a>) (<a href='?_src_=vars;listchange=\ref[DA];index=[index]'>C</a>) (<a href='?_src_=vars;listremove=\ref[DA];index=[index]'>-</a>) "
else
header = "<li style='backgroundColor:white'>(<a href='?_src_=vars;datumedit=\ref[DA];varnameedit=[name]'>E</a>) (<a href='?_src_=vars;datumchange=\ref[DA];varnamechange=[name]'>C</a>) (<a href='?_src_=vars;datummass=\ref[DA];varnamemass=[name]'>M</a>) "
else
header = "<li>"
var/item
if (isnull(value))
item = "[VV_HTML_ENCODE(name)] = <span class='value'>null</span>"
else if (istext(value))
item = "[VV_HTML_ENCODE(name)] = <span class='value'>\"[VV_HTML_ENCODE(value)]\"</span>"
else if (isicon(value))
#ifdef VARSICON
var/icon/I = new/icon(value)
var/rnd = rand(1,10000)
var/rname = "tmp\ref[I][rnd].png"
usr << browse_rsc(I, rname)
item = "[VV_HTML_ENCODE(name)] = (<span class='value'>[value]</span>) <img class=icon src=\"[rname]\">"
#else
item = "[VV_HTML_ENCODE(name)] = /icon (<span class='value'>[value]</span>)"
#endif
/* else if (istype(value, /image))
#ifdef VARSICON
var/rnd = rand(1, 10000)
var/image/I = value
src << browse_rsc(I.icon, "tmp\ref[value][rnd].png")
html += "[name] = <img src=\"tmp\ref[value][rnd].png\">"
#else
html += "[name] = /image (<span class='value'>[value]</span>)"
#endif
*/
else if (isfile(value))
item = "[VV_HTML_ENCODE(name)] = <span class='value'>'[value]'</span>"
//else if (istype(value, /client))
// var/client/C = value
// item = "<a href='?_src_=vars;Vars=\ref[value]'>[VV_HTML_ENCODE(name)] \ref[value]</a> = [C] [C.type]"
else if (istype(value, /datum))
var/datum/D = value
if ("[D]" != "[D.type]") //if the thing as a name var, lets use it.
item = "<a href='?_src_=vars;Vars=\ref[value]'>[VV_HTML_ENCODE(name)] \ref[value]</a> = [D] [D.type]"
else
item = "<a href='?_src_=vars;Vars=\ref[value]'>[VV_HTML_ENCODE(name)] \ref[value]</a> = [D.type]"
else if (islist(value))
var/list/L = value
var/list/items = list()
if (L.len > 0 && !(name == "underlays" || name == "overlays" || L.len > (IS_NORMAL_LIST(L) ? 50 : 150)))
for (var/i in 1 to L.len)
var/key = L[i]
var/val
if (IS_NORMAL_LIST(L) && !isnum(key))
val = L[key]
if (!val)
val = key
key = i
items += debug_variable(key, val, level + 1, sanitize = sanitize)
item = "<a href='?_src_=vars;Vars=\ref[value]'>[VV_HTML_ENCODE(name)] = /list ([L.len])</a><ul>[items.Join()]</ul>"
else
item = "<a href='?_src_=vars;Vars=\ref[value]'>[VV_HTML_ENCODE(name)] = /list ([L.len])</a>"
else
item = "[VV_HTML_ENCODE(name)] = <span class='value'>[VV_HTML_ENCODE(value)]</span>"
return "[header][item]</li>"
#undef VV_HTML_ENCODE
/client/proc/view_var_Topic(href, href_list, hsrc)
if( (usr.client != src) || !src.holder )
return
if(href_list["Vars"])
debug_variables(locate(href_list["Vars"]))
else if(href_list["datumrefresh"])
var/datum/DAT = locate(href_list["datumrefresh"])
if(!DAT) //can't be an istype() because /client etc aren't datums
return
src.debug_variables(DAT)
else if(href_list["mob_player_panel"])
if(!check_rights(0))
return
var/atom/A = D
if(isliving(A))
atomsnowflake += "<a href='?_src_=vars;[HrefToken()];rename=[refid]'><b>[D]</b></a>"
@@ -934,7 +486,6 @@
if(!check_rights(0))
return
>>>>>>> 84b1e3d... [s] Adds a security token to all admin hrefs (#29839)
var/mob/M = locate(href_list["mob_player_panel"]) in GLOB.mob_list
if(!istype(M))
to_chat(usr, "This can only be used on instances of type /mob")
@@ -995,22 +546,6 @@
to_chat(usr, "This can only be done to instances of type /mob")
return
M.regenerate_icons()
//Needs +VAREDIT past this point
else if(check_rights(R_VAREDIT))
//~CARN: for renaming mobs (updates their name, real_name, mind.name, their ID/PDA and datacore records).
if(href_list["rename"])
if(!check_rights(0))
return
if(!ismob(M))
to_chat(usr, "This can only be done to instances of type /mob")
return
M.regenerate_icons()
else if(href_list["expose"])
if(!check_rights(R_ADMIN, FALSE))
return
@@ -1043,7 +578,6 @@
if(!check_rights(0))
return
>>>>>>> 84b1e3d... [s] Adds a security token to all admin hrefs (#29839)
var/mob/M = locate(href_list["rename"]) in GLOB.mob_list
if(!istype(M))
to_chat(usr, "This can only be used on instances of type /mob")

View File

@@ -6,8 +6,14 @@
var/date
/datum/getrev/New()
if(world.RunningService() && fexists(SERVICE_PR_TEST_JSON))
testmerge = json_decode(file2text(SERVICE_PR_TEST_JSON))
if(world.RunningService())
var/file_name
if(ServiceVersion()) //will return null for versions < 3.0.91.0
file_name = SERVICE_PR_TEST_JSON_OLD
else
file_name = SERVICE_PR_TEST_JSON
if(fexists(file_name))
testmerge = json_decode(file2text(file_name))
#ifdef SERVERTOOLS
else if(!world.RunningService() && fexists("../prtestjob.lk")) //tgs2 support
var/list/tmp = world.file2list("..\\prtestjob.lk")

View File

@@ -304,6 +304,7 @@
/atom/proc/ex_act(severity, target)
set waitfor = FALSE
contents_explosion(severity, target)
SendSignal(COMSIG_ATOM_EX_ACT, severity, target)
/atom/proc/blob_act(obj/structure/blob/B)
return
@@ -468,8 +469,8 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
/atom/proc/singularity_act()
return
/atom/proc/singularity_pull()
return
/atom/proc/singularity_pull(obj/singularity/S, current_size)
SendSignal(COMSIG_ATOM_SING_PULL, S, current_size)
/atom/proc/acid_act(acidpwr, acid_volume)
return

View File

@@ -215,7 +215,7 @@
//to differentiate it, naturally everyone forgot about this immediately and so some things
//would bump twice, so now it's called Collide
/atom/movable/proc/Collide(atom/A)
if((A))
if(A)
if(throwing)
throwing.hit_atom(A)
. = 1

View File

@@ -363,7 +363,7 @@
to_chat(src, "<i>Node Blobs</i> are blobs which grow, like the core. Like the core it can activate resource and factory blobs.")
to_chat(src, "<b>In addition to the buttons on your HUD, there are a few click shortcuts to speed up expansion and defense.</b>")
to_chat(src, "<b>Shortcuts:</b> Click = Expand Blob <b>|</b> Middle Mouse Click = Rally Spores <b>|</b> Ctrl Click = Create Shield Blob <b>|</b> Alt Click = Remove Blob")
to_chat(src, "Attempting to talk will send a message to all other GLOB.overminds, allowing you to coordinate with them.")
to_chat(src, "Attempting to talk will send a message to all other overminds, allowing you to coordinate with them.")
if(!placed && autoplace_max_time <= world.time)
to_chat(src, "<span class='big'><font color=\"#EE4000\">You will automatically place your blob core in [round((autoplace_max_time - world.time)/600, 0.5)] minutes.</font></span>")
to_chat(src, "<span class='big'><font color=\"#EE4000\">You [manualplace_min_time ? "will be able to":"can"] manually place your blob core by pressing the Place Blob Core button in the bottom right corner of the screen.</font></span>")

View File

@@ -90,8 +90,10 @@
/obj/structure/blob/proc/Life()
return
/obj/structure/blob/proc/Pulse_Area(pulsing_overmind = overmind, claim_range = 10, pulse_range = 3, expand_range = 2)
src.Be_Pulsed()
/obj/structure/blob/proc/Pulse_Area(mob/camera/blob/pulsing_overmind, claim_range = 10, pulse_range = 3, expand_range = 2)
if(QDELETED(pulsing_overmind))
pulsing_overmind = overmind
Be_Pulsed()
var/expanded = FALSE
if(prob(70) && expand())
expanded = TRUE

View File

@@ -26,8 +26,8 @@
to_chat(user, "<span class='inathneq'>An emergency shuttle has arrived and this prism is no longer useful; attempt to activate it to gain a partial refund of components used.</span>")
else
var/efficiency = get_efficiency_mod(TRUE)
to_chat(user, "<span class='inathneq_small'>It requires at least <b>[get_delay_cost()]W</b> of power to attempt to delay the arrival of an emergency shuttle by <b>[2 * efficiency]</b> minutes.</span>")
to_chat(user, "<span class='inathneq_small'>This cost increases by <b>[delay_cost_increase]W</b> for every previous activation.</span>")
to_chat(user, "<span class='inathneq_small'>It requires at least <b>[DisplayPower(get_delay_cost())]</b> of power to attempt to delay the arrival of an emergency shuttle by <b>[2 * efficiency]</b> minutes.</span>")
to_chat(user, "<span class='inathneq_small'>This cost increases by <b>[DisplayPower(delay_cost_increase)]</b> for every previous activation.</span>")
/obj/structure/destructible/clockwork/powered/prolonging_prism/forced_disable(bad_effects)
if(active)
@@ -126,7 +126,7 @@
if(!hex_combo)
hex_combo = mutable_appearance('icons/effects/64x64.dmi', n, RIPPLE_LAYER)
else
hex_combo.overlays += mutable_appearance('icons/effects/64x64.dmi', n, RIPPLE_LAYER)
hex_combo.add_overlay(mutable_appearance('icons/effects/64x64.dmi', n, RIPPLE_LAYER))
if(hex_combo) //YOU BUILT A HEXAGON
hex_combo.pixel_x = -16
hex_combo.pixel_y = -16

View File

@@ -672,7 +672,7 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list(
var/obj/machinery/transformer/conveyor = new(T)
conveyor.masterAI = owner
playsound(T, 'sound/effects/phasein.ogg', 100, 1)
owner_AI.can_shunt = TRUE
owner_AI.can_shunt = FALSE
to_chat(owner, "<span class='warning'>You are no longer able to shunt your core to APCs.</span>")
adjust_uses(-1)

View File

@@ -90,6 +90,7 @@
playsound(loc, 'sound/effects/glassbr3.ogg', 100, 1)
stat |= BROKEN
update_icon()
set_light(0)
/obj/machinery/computer/emp_act(severity)
switch(severity)

View File

@@ -213,7 +213,7 @@
sleep(6)
operating = FALSE
desc += "<BR><span class='warning'>Its access panel is smoking slightly.</span>"
open()
open(2)
/obj/machinery/door/window/attackby(obj/item/I, mob/living/user, params)

View File

@@ -58,7 +58,9 @@
/turf/open/floor/plating/asteroid/drill_act(obj/item/mecha_parts/mecha_equipment/drill/drill)
for(var/turf/open/floor/plating/asteroid/M in range(1, drill.chassis))
if(get_dir(drill.chassis,M)&drill.chassis.dir)
M.gets_dug()
for(var/I in GetComponents(/datum/component/archaeology))
var/datum/component/archaeology/archy = I
archy.gets_dug()
drill.log_message("Drilled through [src]")
drill.move_ores()

View File

@@ -549,9 +549,10 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE)
transfer_blood = 0
/obj/item/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FOUR)
throw_at(S,14,3, spin=0)
else ..()
else return
/obj/item/throw_impact(atom/A)
if(A && !QDELETED(A))

View File

@@ -428,7 +428,7 @@
/obj/item/device/flashlight/glowstick/update_icon()
item_state = "glowstick"
overlays.Cut()
cut_overlays()
if(!fuel)
icon_state = "glowstick-empty"
cut_overlays()

View File

@@ -86,7 +86,7 @@
icon_state = null
active = TRUE
if(tile_overlay)
loc.overlays += tile_overlay
loc.add_overlay(tile_overlay)
else
if(crossed)
trigger() //no cheesing.

View File

@@ -1,27 +1,201 @@
/obj/item/banner
name = "banner"
desc = "A banner with Nanotrasen's logo on it."
icon = 'icons/obj/items_and_weapons.dmi'
icon_state = "banner"
item_state = "banner"
force = 8
attack_verb = list("forcefully inspired", "violently encouraged", "relentlessly galvanized")
lefthand_file = 'icons/mob/inhands/equipment/banners_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/banners_righthand.dmi'
desc = "A banner with Nanotrasen's logo on it."
var/moralecooldown = 0
var/moralewait = 600
var/inspiration_available = TRUE //If this banner can be used to inspire crew
var/morale_time = 0
var/morale_cooldown = 600 //How many deciseconds between uses
var/list/job_loyalties //Mobs with any of these assigned roles will be inspired
var/list/role_loyalties //Mobs with any of these special roles will be inspired
var/warcry
/obj/item/banner/examine(mob/user)
..()
if(inspiration_available)
to_chat(user, "<span class='notice'>Activate it in your hand to inspire nearby allies of this banner's allegiance!</span>")
/obj/item/banner/attack_self(mob/living/carbon/human/user)
if(moralecooldown + moralewait > world.time)
if(!inspiration_available)
return
to_chat(user, "<span class='notice'>You increase the morale of your fellows!</span>")
moralecooldown = world.time
if(morale_time > world.time)
to_chat(user, "<span class='warning'>You aren't feeling inspired enough to flourish [src] again yet.</span>")
return
user.visible_message("<span class='big notice'>[user] flourishes [src]!</span>", \
"<span class='notice'>You raise [src] skywards, inspiring your allies!</span>")
playsound(src, "rustle", 100, FALSE)
if(warcry)
user.say("[warcry]")
var/old_transform = user.transform
user.transform *= 1.2
animate(user, transform = old_transform, time = 10)
morale_time = world.time + morale_cooldown
for(var/mob/living/carbon/human/H in range(4,get_turf(src)))
to_chat(H, "<span class='notice'>Your morale is increased by [user]'s banner!</span>")
H.adjustBruteLoss(-15)
H.adjustFireLoss(-15)
H.AdjustStun(-40)
H.AdjustKnockdown(-40)
H.AdjustUnconscious(-40)
var/list/inspired = list()
var/has_job_loyalties = LAZYLEN(job_loyalties)
var/has_role_loyalties = LAZYLEN(role_loyalties)
inspired += user //The user is always inspired, regardless of loyalties
for(var/mob/living/carbon/human/H in range(4, get_turf(src)))
if(H.stat == DEAD || H == user)
continue
if(H.mind && (has_job_loyalties || has_role_loyalties))
if(has_job_loyalties && H.mind.assigned_role in job_loyalties)
inspired += H
else if(has_role_loyalties && H.mind.special_role in role_loyalties)
inspired += H
else if(check_inspiration(H))
inspired += H
for(var/V in inspired)
var/mob/living/carbon/human/H = V
if(H != user)
to_chat(H, "<span class='notice'>Your confidence surges as [user] flourishes [user.p_their()] [name]!</span>")
inspiration(H)
special_inspiration(H)
/obj/item/banner/proc/check_inspiration(mob/living/carbon/human/H) //Banner-specific conditions for being eligible
return
/obj/item/banner/proc/inspiration(mob/living/carbon/human/H)
H.adjustBruteLoss(-15)
H.adjustFireLoss(-15)
H.AdjustStun(-40)
H.AdjustKnockdown(-40)
H.AdjustUnconscious(-40)
playsound(H, 'sound/magic/staff_healing.ogg', 25, FALSE)
/obj/item/banner/proc/special_inspiration(mob/living/carbon/human/H) //Any banner-specific inspiration effects go here
return
/obj/item/banner/security
name = "securistan banner"
desc = "The banner of Securistan, ruling the station with an iron fist."
icon_state = "banner_security"
job_loyalties = list("Security Officer", "Warden", "Detective", "Head of Security")
warcry = "EVERYONE DOWN ON THE GROUND!!"
/obj/item/banner/security/mundane
inspiration_available = FALSE
/datum/crafting_recipe/security_banner
name = "Securistan Banner"
result = /obj/item/banner/security/mundane
time = 40
reqs = list(/obj/item/stack/rods = 2,
/obj/item/clothing/under/rank/security = 1)
category = CAT_MISC
/obj/item/banner/medical
name = "meditopia banner"
desc = "The banner of Meditopia, generous benefactors that cure wounds and shelter the weak."
icon_state = "banner_medical"
job_loyalties = list("Medical Doctor", "Chemist", "Geneticist", "Virologist", "Chief Medical Officer")
warcry = "No wounds cannot be healed!"
/obj/item/banner/medical/mundane
inspiration_available = FALSE
/obj/item/banner/medical/check_inspiration(mob/living/carbon/human/H)
return H.stat //Meditopia is moved to help those in need
/datum/crafting_recipe/medical_banner
name = "Meditopia Banner"
result = /obj/item/banner/medical/mundane
time = 40
reqs = list(/obj/item/stack/rods = 2,
/obj/item/clothing/under/rank/medical = 1)
category = CAT_MISC
/obj/item/banner/medical/special_inspiration(mob/living/carbon/human/H)
H.adjustToxLoss(-15)
H.setOxyLoss(0)
H.reagents.add_reagent("inaprovaline", 5)
/obj/item/banner/science
name = "sciencia banner"
desc = "The banner of Sciencia, bold and daring thaumaturges and researchers that take the path less traveled."
icon_state = "banner_science"
job_loyalties = list("Scientist", "Roboticist", "Research Director")
warcry = "For Cuban Pete!"
/obj/item/banner/science/mundane
inspiration_available = FALSE
/obj/item/banner/science/check_inspiration(mob/living/carbon/human/H)
return H.on_fire //Sciencia is pleased by dedication to the art of Toxins
/datum/crafting_recipe/science_banner
name = "Sciencia Banner"
result = /obj/item/banner/science/mundane
time = 40
reqs = list(/obj/item/stack/rods = 2,
/obj/item/clothing/under/rank/scientist = 1)
category = CAT_MISC
/obj/item/banner/cargo
name = "cargonia banner"
desc = "The banner of the eternal Cargonia, with the mystical power of conjuring any object into existence."
icon_state = "banner_cargo"
job_loyalties = list("Cargo Technician", "Shaft Miner", "Quartermaster")
warcry = "Hail Cargonia!"
/obj/item/banner/cargo/mundane
inspiration_available = FALSE
/datum/crafting_recipe/cargo_banner
name = "Cargonia Banner"
result = /obj/item/banner/cargo/mundane
time = 40
reqs = list(/obj/item/stack/rods = 2,
/obj/item/clothing/under/rank/cargotech = 1)
category = CAT_MISC
/obj/item/banner/engineering
name = "engitopia banner"
desc = "The banner of Engitopia, wielders of limitless power."
icon_state = "banner_engineering"
job_loyalties = list("Station Engineer", "Atmospheric Technician", "Chief Engineer")
warcry = "All hail lord Singuloth!!"
/obj/item/banner/engineering/mundane
inspiration_available = FALSE
/obj/item/banner/engineering/special_inspiration(mob/living/carbon/human/H)
H.radiation = 0
/datum/crafting_recipe/engineering_banner
name = "Engitopia Banner"
result = /obj/item/banner/engineering/mundane
time = 40
reqs = list(/obj/item/stack/rods = 2,
/obj/item/clothing/under/rank/engineer = 1)
category = CAT_MISC
/obj/item/banner/command
name = "command banner"
desc = "The banner of Command, a staunch and ancient line of bueraucratic kings and queens."
//No icon state here since the default one is the NT banner
job_loyalties = list("Captain", "Head of Personnel", "Chief Engineer", "Head of Security", "Research Director", "Chief Medical Officer")
warcry = "Hail Nanotrasen!"
/obj/item/banner/command/mundane
inspiration_available = FALSE
/obj/item/banner/command/check_inspiration(mob/living/carbon/human/H)
return H.isloyal() //Command is stalwart but rewards their allies.
/datum/crafting_recipe/command_banner
name = "Command Banner"
result = /obj/item/banner/command/mundane
time = 40
reqs = list(/obj/item/stack/rods = 2,
/obj/item/clothing/under/captainparade = 1)
category = CAT_MISC
/obj/item/banner/red
name = "red banner"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -169,6 +169,7 @@
return
/obj/singularity_pull(S, current_size)
..()
if(!anchored || current_size >= STAGE_FIVE)
step_towards(src,S)

View File

@@ -7,6 +7,7 @@
var/height = 0 //the 'height' of the ladder. higher numbers are considered physically higher
var/obj/structure/ladder/down = null //the ladder below this one
var/obj/structure/ladder/up = null //the ladder above this one
var/auto_connect = FALSE
/obj/structure/ladder/unbreakable //mostly useful for awaymissions to prevent halting progress in a mission
name = "sturdy ladder"
@@ -19,12 +20,18 @@
return INITIALIZE_HINT_LATELOAD
/obj/structure/ladder/Destroy()
if(up && up.down == src)
up.down = null
up.update_icon()
if(down && down.up == src)
down.up = null
down.update_icon()
GLOB.ladders -= src
. = ..()
/obj/structure/ladder/LateInitialize()
for(var/obj/structure/ladder/L in GLOB.ladders)
if(L.id == id)
if(L.id == id || (auto_connect && L.auto_connect && L.x == x && L.y == y))
if(L.height == (height - 1))
down = L
continue
@@ -50,31 +57,33 @@
else //wtf make your ladders properly assholes
icon_state = "ladder00"
/obj/structure/ladder/proc/go_up(mob/user,is_ghost)
/obj/structure/ladder/proc/travel(going_up, mob/user, is_ghost, obj/structure/ladder/ladder)
if(!is_ghost)
show_fluff_message(1,user)
up.add_fingerprint(user)
user.loc = get_turf(up)
show_fluff_message(going_up,user)
ladder.add_fingerprint(user)
var/atom/movable/AM
if(user.pulling)
AM = user.pulling
user.pulling.forceMove(get_turf(ladder))
user.forceMove(get_turf(ladder))
if(AM)
user.start_pulling(AM)
/obj/structure/ladder/proc/go_down(mob/user,is_ghost)
if(!is_ghost)
show_fluff_message(0,user)
down.add_fingerprint(user)
user.loc = get_turf(down)
/obj/structure/ladder/proc/use(mob/user,is_ghost=0)
if(up && down)
switch( alert("Go up or down the ladder?", "Ladder", "Up", "Down", "Cancel") )
if("Up")
go_up(user,is_ghost)
travel(TRUE, user, is_ghost, up)
if("Down")
go_down(user,is_ghost)
travel(FALSE, user, is_ghost, down)
if("Cancel")
return
else if(up)
go_up(user,is_ghost)
travel(TRUE, user, is_ghost, up)
else if(down)
go_down(user,is_ghost)
travel(FALSE, user,is_ghost, down)
else
to_chat(user, "<span class='warning'>[src] doesn't seem to lead anywhere!</span>")
@@ -108,3 +117,14 @@
. = ..()
else
return QDEL_HINT_LETMELIVE
/obj/structure/ladder/unbreakable/singularity_pull()
return
/obj/structure/ladder/auto_connect //They will connect to ladders with the same X and Y without needing to share an ID
auto_connect = TRUE
/obj/structure/ladder/singularity_pull()
visible_message("<span class='danger'>[src] is torn to pieces by the gravitational pull!</span>")
qdel(src)

View File

@@ -45,6 +45,7 @@
qdel(src)
/obj/structure/lattice/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FOUR)
deconstruct()

View File

@@ -30,6 +30,7 @@
return ..()
/obj/structure/transit_tube/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
deconstruct(FALSE)

View File

@@ -62,6 +62,7 @@
AM.ex_act(severity, target)
/obj/structure/transit_tube_pod/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
deconstruct(FALSE)

View File

@@ -105,6 +105,7 @@
qdel(src)
/obj/structure/window/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
deconstruct(FALSE)

View File

@@ -4,6 +4,9 @@
var/wet = 0
var/wet_time = 0 // Time in seconds that this floor will be wet for.
var/mutable_appearance/wet_overlay
var/postdig_icon_change = FALSE
var/postdig_icon
var/list/archdrops
/turf/open/indestructible
name = "floor"

View File

@@ -170,6 +170,7 @@
return make_plating()
/turf/open/floor/singularity_pull(S, current_size)
..()
if(current_size == STAGE_THREE)
if(prob(30))
if(floor_tile)

View File

@@ -269,6 +269,7 @@
narsie_act(force, ignore_mobs, probability)
/turf/open/floor/vines/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
if(prob(50))
ChangeTurf(src.baseturf)

View File

@@ -9,11 +9,11 @@
icon = 'icons/turf/floors.dmi'
icon_state = "asteroid"
icon_plating = "asteroid"
postdig_icon_change = TRUE
var/environment_type = "asteroid"
var/turf_type = /turf/open/floor/plating/asteroid //Because caves do whacky shit to revert to normal
var/dug = 0 //0 = has not yet been dug, 1 = has already been dug
var/sand_type = /obj/item/ore/glass
var/floor_variance = 20 //probability floor has a different icon state
archdrops = list(/obj/item/ore/glass = 5)
/turf/open/floor/plating/asteroid/Initialize()
var/proper_name = name
@@ -22,6 +22,9 @@
if(prob(floor_variance))
icon_state = "[environment_type][rand(0,12)]"
if(LAZYLEN(archdrops))
AddComponent(/datum/component/archaeology, 100, archdrops)
/turf/open/floor/plating/asteroid/burn_tile()
return
@@ -31,46 +34,7 @@
/turf/open/floor/plating/asteroid/MakeDry(wet_setting = TURF_WET_WATER)
return
/turf/open/floor/plating/asteroid/ex_act(severity, target)
contents_explosion(severity, target)
switch(severity)
if(3)
return
if(2)
if(prob(20))
src.gets_dug()
if(1)
src.gets_dug()
/turf/open/floor/plating/asteroid/attackby(obj/item/W, mob/user, params)
//note that this proc does not call ..()
if(!W || !user)
return 0
var/digging_speed = 0
if (istype(W, /obj/item/shovel))
var/obj/item/shovel/S = W
digging_speed = S.digspeed
else if (istype(W, /obj/item/pickaxe))
var/obj/item/pickaxe/P = W
digging_speed = P.digspeed
if (digging_speed)
var/turf/T = user.loc
if(!isturf(T))
return
if (dug)
to_chat(user, "<span class='warning'>This area has already been dug!</span>")
return
to_chat(user, "<span class='notice'>You start digging...</span>")
playsound(src, 'sound/effects/shovel_dig.ogg', 50, 1)
if(do_after(user, digging_speed, target = src))
if(istype(src, /turf/open/floor/plating/asteroid))
to_chat(user, "<span class='notice'>You dig a hole.</span>")
gets_dug()
SSblackbox.add_details("pick_used_mining","[W.type]")
if(istype(W, /obj/item/storage/bag/ore))
var/obj/item/storage/bag/ore/S = W
if(S.collection_mode == 1)
@@ -88,37 +52,16 @@
var/turf/open/floor/light/F = T
F.state = L.state
playsound(src, 'sound/weapons/genhit.ogg', 50, 1)
/turf/open/floor/plating/asteroid/proc/gets_dug()
if(dug)
return
for(var/i in 1 to 5)
new sand_type(src)
dug = 1
icon_plating = "[environment_type]_dug"
icon_state = "[environment_type]_dug"
slowdown = 0
return
return ..()
/turf/open/floor/plating/asteroid/singularity_act()
if(turf_z_is_planet(src))
return ..()
ChangeTurf(/turf/open/space)
/turf/open/floor/plating/asteroid/singularity_pull(S, current_size)
if(dug)
return
switch(current_size)
if(STAGE_THREE)
if(!prob(30))
gets_dug()
if(STAGE_FOUR)
if(prob(50))
gets_dug()
else
if(current_size >= STAGE_FIVE && prob(70))
gets_dug()
/turf/open/floor/plating/asteroid/basalt
name = "volcanic floor"
@@ -127,7 +70,7 @@
icon_state = "basalt"
icon_plating = "basalt"
environment_type = "basalt"
sand_type = /obj/item/ore/glass/basalt
archdrops = list(/obj/item/ore/glass/basalt = 5)
floor_variance = 15
/turf/open/floor/plating/asteroid/basalt/lava //lava underneath
@@ -147,10 +90,10 @@
if("basalt5", "basalt9")
B.set_light(1.4, 0.6, LIGHT_COLOR_LAVA) //barely anything!
/turf/open/floor/plating/asteroid/basalt/gets_dug()
if(!dug)
set_light(0)
/turf/open/floor/plating/asteroid/basalt/ComponentActivated(datum/component/C)
..()
if(istype(C, /datum/component/archaeology))
set_light(0)
///////Surface. The surface is warm, but survivable without a suit. Internals are required. The floors break to chasms, which drop you into the underground.
@@ -340,8 +283,8 @@
initial_gas_mix = "TEMP=180"
slowdown = 2
environment_type = "snow"
sand_type = /obj/item/stack/sheet/mineral/snow
flags_1 = NONE
archdrops = list(/obj/item/stack/sheet/mineral/snow = 5)
/turf/open/floor/plating/asteroid/snow/airless
initial_gas_mix = "TEMP=2.7"

View File

@@ -58,6 +58,7 @@
make_plating(1)
/turf/open/floor/engine/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
if(floor_tile)
if(prob(30))

View File

@@ -247,6 +247,7 @@
icon_state = "r_wall"
/turf/closed/wall/r_wall/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
if(prob(30))
dismantle_wall()

View File

@@ -245,6 +245,7 @@
QDEL_IN(O, 50)
/turf/closed/wall/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
if(prob(50))
dismantle_wall()

View File

@@ -524,12 +524,12 @@
/turf/proc/photograph(limit=20)
var/image/I = new()
I.overlays += src
I.add_overlay(src)
for(var/V in contents)
var/atom/A = V
if(A.invisibility)
continue
I.overlays += A
I.add_overlay(A)
if(limit)
limit--
else

View File

@@ -1,3 +1,6 @@
GLOBAL_VAR(security_mode)
GLOBAL_PROTECT(security_mode)
/world/New()
log_world("World loaded at [time_stamp()]")
@@ -5,6 +8,8 @@
GLOB.config_error_log = GLOB.sql_error_log = GLOB.world_href_log = GLOB.world_runtime_log = GLOB.world_attack_log = GLOB.world_game_log = file("data/logs/config_error.log") //temporary file used to record errors with loading config, moved to log directory once logging is set bl
CheckSecurityMode()
make_datum_references_lists() //initialises global lists for referencing frequently used datums (so that we only ever do it once)
config = new
@@ -94,6 +99,20 @@
if(GLOB.round_id)
log_game("Round ID: [GLOB.round_id]")
/world/proc/CheckSecurityMode()
//try to write to data
if(!text2file("The world is running at least safe mode", "data/server_security_check.lock"))
GLOB.security_mode = SECURITY_ULTRASAFE
warning("/tg/station 13 is not supported in ultrasafe security mode. Everything will break!")
return
//try to shell
if(shell("echo \"The world is running in trusted mode\"") != null)
GLOB.security_mode = SECURITY_TRUSTED
else
GLOB.security_mode = SECURITY_SAFE
warning("/tg/station 13 uses many file operations, a few shell()s, and some external call()s. Trusted mode is recommended. You can download our source code for your own browsing and compilation at https://github.com/tgstation/tgstation")
/world/Topic(T, addr, master, key)
var/list/input = params2list(T)

View File

@@ -261,6 +261,8 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
verbs += GLOB.admin_verbs_poll
if(rights & R_SOUNDS)
verbs += GLOB.admin_verbs_sounds
if(config.invoke_youtubedl)
verbs += /client/proc/play_web_sound
if(rights & R_SPAWN)
verbs += GLOB.admin_verbs_spawn
@@ -283,6 +285,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
/client/proc/stealth,
GLOB.admin_verbs_poll,
GLOB.admin_verbs_sounds,
/client/proc/play_web_sound,
GLOB.admin_verbs_spawn,
/*Debug verbs added by "show debug verbs"*/
/client/proc/Cell,

View File

@@ -22,6 +22,8 @@
return 0
/proc/jobban_buildcache(client/C)
if(!SSdbcore.Connect())
return
if(C && istype(C))
C.jobbancache = list()
var/datum/DBQuery/query_jobban_build_cache = SSdbcore.NewQuery("SELECT job, reason FROM [format_table_name("ban")] WHERE ckey = '[sanitizeSQL(C.ckey)]' AND (bantype = 'JOB_PERMABAN' OR (bantype = 'JOB_TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned)")

View File

@@ -36,7 +36,11 @@
for(var/mob/M in GLOB.player_list)
if(M.client.prefs.toggles & SOUND_MIDI)
var/user_vol = M.client.chatOutput.adminMusicVolume
if(user_vol)
admin_sound.volume = vol * (user_vol / 100)
SEND_SOUND(M, admin_sound)
admin_sound.volume = vol
SSblackbox.add_details("admin_verb","Play Global Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
@@ -52,6 +56,62 @@
playsound(get_turf(src.mob), S, 50, 0, 0)
SSblackbox.add_details("admin_verb","Play Local Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/play_web_sound()
set category = "Fun"
set name = "Play Internet Sound"
if(!check_rights(R_SOUNDS))
return
if(!config.invoke_youtubedl)
to_chat(src, "<span class='boldwarning'>Youtube-dl was not configured, action unavailable</span>") //Check config.txt for the INVOKE_YOUTUBEDL value
return
var/web_sound_input = input("Enter content URL (supported sites only, leave blank to stop playing)", "Play Internet Sound via youtube-dl") as text|null
if(istext(web_sound_input))
var/web_sound_url = ""
var/pitch
if(length(web_sound_input))
web_sound_input = trim(web_sound_input)
var/static/regex/html_protocol_regex = regex("https?://")
if(findtext(web_sound_input, ":") && !findtext(web_sound_input, html_protocol_regex))
to_chat(src, "<span class='boldwarning'>Non-http(s) URIs are not allowed.</span>")
to_chat(src, "<span class='warning'>For youtube-dl shortcuts like ytsearch: please use the appropriate full url from the website.</span>")
return
var/shell_scrubbed_input = shell_url_scrub(web_sound_input)
var/list/output = world.shelleo("[config.invoke_youtubedl] --format \"bestaudio\[ext=aac]/bestaudio\[ext=mp3]/bestaudio\[ext=m4a]\" --get-url \"[shell_scrubbed_input]\"")
var/errorlevel = output[SHELLEO_ERRORLEVEL]
var/stdout = output[SHELLEO_STDOUT]
var/stderr = output[SHELLEO_STDERR]
if(!errorlevel)
var/static/regex/content_url_regex = regex("https?://\\S+")
if(content_url_regex.Find(stdout))
web_sound_url = content_url_regex.match
if(SSevents.holidays && SSevents.holidays[APRIL_FOOLS])
pitch = pick(0.5, 0.7, 0.8, 0.85, 0.9, 0.95, 1.1, 1.2, 1.4, 1.6, 2.0, 2.5)
to_chat(src, "You feel the Honkmother messing with your song...")
log_admin("[key_name(src)] played web sound: [web_sound_input]")
message_admins("[key_name(src)] played web sound: [web_sound_input]")
else
to_chat(src, "<span class='boldwarning'>Youtube-dl URL retrieval FAILED:</span>")
to_chat(src, "<span class='warning'>[stderr]</span>")
else //pressed ok with blank
log_admin("[key_name(src)] stopped web sound")
message_admins("[key_name(src)] stopped web sound")
web_sound_url = " "
if(web_sound_url)
for(var/m in GLOB.player_list)
var/mob/M = m
var/client/C = M.client
if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
C.chatOutput.sendMusic(web_sound_url, pitch)
SSblackbox.add_details("admin_verb","Play Internet Sound")
/client/proc/set_round_end_sound(S as sound)
set category = "Fun"
set name = "Set Round End Sound"
@@ -75,4 +135,7 @@
for(var/mob/M in GLOB.player_list)
if(M.client)
SEND_SOUND(M, sound(null))
var/client/C = M.client
if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
C.chatOutput.sendMusic(" ")
SSblackbox.add_details("admin_verb","Stop All Playing Sounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

View File

@@ -222,6 +222,7 @@ Pipelines + Other Objects -> Pipe network
build_network()
/obj/machinery/atmospherics/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
deconstruct(FALSE)

View File

@@ -120,6 +120,7 @@
return 1
/obj/machinery/meter/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
new /obj/item/pipe_meter(loc)
qdel(src)

View File

@@ -144,6 +144,9 @@ TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, togglemidis)()
else
to_chat(usr, "You will no longer hear sounds uploaded by admins")
usr.stop_sound_channel(CHANNEL_ADMIN)
var/client/C = usr.client
if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
C.chatOutput.sendMusic(" ")
SSblackbox.add_details("preferences_verb","Toggle Hearing Midis|[usr.client.prefs.toggles & SOUND_MIDI]") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/datum/verbs/menu/Settings/Sound/togglemidis/Get_checked(client/C)
return C.prefs.toggles & SOUND_MIDI
@@ -230,6 +233,9 @@ TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggleprayersounds)()
set category = "Preferences"
set desc = "Stop Current Sounds"
SEND_SOUND(usr, sound(null))
var/client/C = usr.client
if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
C.chatOutput.sendMusic(" ")
SSblackbox.add_details("preferences_verb","Stop Self Sounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

View File

@@ -142,69 +142,55 @@
user.set_machine(src)
interact(user)
/*******************
* SmartFridge Menu
********************/
/obj/machinery/smartfridge/interact(mob/user)
if(stat)
return FALSE
var/dat = "<TT><b>Select an item:</b><br>"
/obj/machinery/smartfridge/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
ui = new(user, src, ui_key, "smartvend", name, 440, 550, master_ui, state)
ui.open()
if (contents.len == 0)
dat += "<font color = 'red'>No product loaded!</font>"
else
var/listofitems = list()
for (var/atom/movable/O in contents)
if (listofitems[O.name])
listofitems[O.name]++
else
listofitems[O.name] = 1
sortList(listofitems)
/obj/machinery/smartfridge/ui_data(mob/user)
. = list()
for (var/O in listofitems)
if(listofitems[O] <= 0)
continue
var/N = listofitems[O]
var/itemName = url_encode(O)
dat += "<FONT color = 'blue'><B>[capitalize(O)]</B>:"
dat += " [N] </font>"
dat += "<a href='byond://?src=\ref[src];vend=[itemName];amount=1'>Vend</A> "
if(N > 5)
dat += "(<a href='byond://?src=\ref[src];vend=[itemName];amount=5'>x5</A>)"
if(N > 10)
dat += "(<a href='byond://?src=\ref[src];vend=[itemName];amount=10'>x10</A>)"
if(N > 25)
dat += "(<a href='byond://?src=\ref[src];vend=[itemName];amount=25'>x25</A>)"
if(N > 1)
dat += "(<a href='?src=\ref[src];vend=[itemName];amount=[N]'>All</A>)"
var/listofitems = list()
for (var/I in src)
var/atom/movable/O = I
if (listofitems[O.name])
listofitems[O.name]["amount"]++
else
listofitems[O.name] = list("name" = O.name, "type" = O.type, "amount" = 1)
sortList(listofitems)
dat += "<br>"
.["contents"] = listofitems
.["name"] = name
.["isdryer"] = FALSE
dat += "</TT>"
user << browse("<HEAD><TITLE>[src] supplies</TITLE></HEAD><TT>[dat]</TT>", "window=smartfridge")
onclose(user, "smartfridge")
return dat
/obj/machinery/smartfridge/Topic(var/href, var/list/href_list)
if(..())
/obj/machinery/smartfridge/ui_act(action, params)
. = ..()
if(.)
return
usr.set_machine(src)
switch(action)
if("Release")
var/desired = 0
var/N = href_list["vend"]
var/amount = text2num(href_list["amount"])
if (params["amount"])
desired = text2num(params["amount"])
else
desired = input("How many items?", "How many items would you like to take out?", 1) as null|num
var/i = amount
for(var/obj/O in contents)
if(i <= 0)
break
if(O.name == N)
O.loc = src.loc
i--
if(QDELETED(src) || QDELETED(usr) || !usr.Adjacent(src)) // Sanity checkin' in case stupid stuff happens while we wait for input()
return FALSE
updateUsrDialog()
for(var/obj/item/O in src)
if(desired <= 0)
break
if(O.name == params["name"])
O.forceMove(drop_location())
desired--
return TRUE
return FALSE
// ----------------------------
@@ -240,20 +226,35 @@
/obj/machinery/smartfridge/drying_rack/default_deconstruction_crowbar(obj/item/crowbar/C, ignore_panel = 1)
..()
/obj/machinery/smartfridge/drying_rack/interact(mob/user)
var/dat = ..()
if(dat)
dat += "<br>"
dat += "<a href='byond://?src=\ref[src];dry=1'>Toggle Drying</A> "
user << browse("<HEAD><TITLE>[src] supplies</TITLE></HEAD><TT>[dat]</TT>", "window=smartfridge")
onclose(user, "smartfridge")
/obj/machinery/smartfridge/drying_rack/ui_data(mob/user)
. = list()
/obj/machinery/smartfridge/drying_rack/Topic(href, list/href_list)
..()
if(href_list["dry"])
toggle_drying(FALSE)
updateUsrDialog()
update_icon()
var/listofitems = list()
for (var/I in src)
var/atom/movable/O = I
if (listofitems[O.name])
listofitems[O.name]["amount"]++
else
listofitems[O.name] = list("name" = O.name, "type" = O.type, "amount" = 1)
sortList(listofitems)
.["contents"] = listofitems
.["name"] = name
.["isdryer"] = TRUE
.["verb"] = "Take"
.["drying"] = drying
/obj/machinery/smartfridge/drying_rack/ui_act(action, params)
. = ..()
if(.)
return
switch(action)
if("Dry")
toggle_drying(FALSE)
return TRUE
return FALSE
/obj/machinery/smartfridge/drying_rack/power_change()
if(powered() && anchored)

View File

@@ -13,6 +13,7 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic
var/cookieSent = FALSE // Has the client sent a cookie for analysis
var/broken = FALSE
var/list/connectionHistory //Contains the connection history passed from chat cookie
var/adminMusicVolume = 100 //This is for the Play Global Sound verb
/datum/chatOutput/New(client/C)
owner = C
@@ -79,6 +80,9 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic
if("analyzeClientData")
data = analyzeClientData(arglist(params))
if("setMusicVolume")
data = setMusicVolume(arglist(params))
if(data)
ehjax_send(data = data)
@@ -120,6 +124,16 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic
data = json_encode(data)
C << output("[data]", "[window]:ehjaxCallback")
/datum/chatOutput/proc/sendMusic(music, pitch)
var/list/music_data = list("adminMusic" = url_encode(url_encode(music)))
if(pitch)
music_data["musicRate"] = pitch
ehjax_send(data = music_data)
/datum/chatOutput/proc/setMusicVolume(volume = "")
if(volume)
adminMusicVolume = Clamp(text2num(volume), 0, 100)
//Sends client connection details to the chat to handle and save
/datum/chatOutput/proc/sendClientData()
//Get dem deets

View File

@@ -101,7 +101,7 @@ a.popt {text-decoration: none;}
top: 0;
right: 0;
}
#options a {
#options .optionsCell {
background: #ddd;
height: 30px;
padding: 5px 0;
@@ -111,7 +111,7 @@ a.popt {text-decoration: none;}
line-height: 28px;
border-top: 1px solid #b4b4b4;
}
#options a:hover {background: #ccc;}
#options .optionsCell:hover {background: #ccc;}
#options .toggle {
width: 40px;
background: #ccc;
@@ -121,7 +121,7 @@ a.popt {text-decoration: none;}
}
#options .sub {clear: both; display: none; width: 160px;}
#options .sub.scroll {overflow-y: scroll;}
#options .sub a {padding: 3px 0 3px 8px; line-height: 30px; font-size: 0.9em; clear: both;}
#options .sub.optionsCell {padding: 3px 0 3px 8px; line-height: 30px; font-size: 0.9em; clear: both;}
#options .sub span {
display: block;
line-height: 30px;
@@ -136,6 +136,13 @@ a.popt {text-decoration: none;}
line-height: 30px;
float: right;
}
#options .sub input {
position: absolute;
padding: 7px 5px;
width: 121px;
line-height: 30px;
float: left;
}
#options .decreaseFont {border-top: 0;}
/* POPUPS */

View File

@@ -28,17 +28,19 @@
<span class="ms" id="pingMs">--ms</span>
</div>
<div id="options">
<a href="#" class="toggle" id="toggleOptions" title="Options"><i class="icon-cog"></i></a>
<a href="#" class="optionsCell toggle" id="toggleOptions" title="Options"><i class="icon-cog"></i></a>
<div class="sub" id="subOptions">
<a href="#" class="decreaseFont" id="decreaseFont"><span>Decrease font size</span> <i class="icon-font">-</i></a>
<a href="#" class="increaseFont" id="increaseFont"><span>Increase font size</span> <i class="icon-font">+</i></a>
<a href="#" class="togglePing" id="togglePing"><span>Toggle ping display</span> <i class="icon-circle"></i></a>
<a href="#" class="highlightTerm" id="highlightTerm"><span>Highlight string</span> <i class="icon-tag"></i></a>
<a href="#" class="saveLog" id="saveLog"><span>Save chat log</span> <i class="icon-save"></i></a>
<a href="#" class="clearMessages" id="clearMessages"><span>Clear all messages</span> <i class="icon-eraser"></i></a>
<a href="#" class="optionsCell decreaseFont" id="decreaseFont"><span>Decrease font size</span> <i class="icon-font">-</i></a>
<a href="#" class="optionsCell increaseFont" id="increaseFont"><span>Increase font size</span> <i class="icon-font">+</i></a>
<a href="#" class="optionsCell togglePing" id="togglePing"><span>Toggle ping display</span> <i class="icon-circle"></i></a>
<a href="#" class="optionsCell highlightTerm" id="highlightTerm"><span>Highlight string</span> <i class="icon-tag"></i></a>
<a href="#" class="optionsCell saveLog" id="saveLog"><span>Save chat log</span> <i class="icon-save"></i></a>
<a href="#" class="optionsCell clearMessages" id="clearMessages"><span>Clear all messages</span> <i class="icon-eraser"></i></a>
<span class="optionsCell" id="musicVolumeSpan"><input type="range" class="hidden" id="musicVolume"><span id="musicVolumeText">Admin music volume</span><i class="icon-music"></i></span>
</div>
</div>
</div>
<audio class="hidden" id="adminMusic" autoplay></audio>
<script type="text/javascript" src="browserOutput.js"></script>
</body>
</html>

View File

@@ -61,8 +61,17 @@ var opts = {
'clientDataLimit': 5,
'clientData': [],
//Admin music volume update
'volumeUpdateDelay': 5000, //Time from when the volume updates to data being sent to the server
'volumeUpdating': false, //True if volume update function set to fire
'updatedVolume': 0, //The volume level that is sent to the server
};
function clamp(val, min, max) {
return Math.max(min, Math.min(val, max))
}
function outerHTML(el) {
var wrap = document.createElement('div');
wrap.appendChild(el.cloneNode(true));
@@ -95,6 +104,15 @@ function linkify(text) {
});
}
function byondDecode(message) {
// Basically we url_encode twice server side so we can manually read the encoded version and actually do UTF-8.
// The replace for + is because FOR SOME REASON, BYOND replaces spaces with a + instead of %20, and a plus with %2b.
// Marvelous.
message = message.replace(/\+/g, "%20");
message = decoder(message);
return message;
}
//Actually turns the highlight term match into appropriate html
function addHighlightMarkup(match) {
var extra = '';
@@ -176,11 +194,7 @@ function output(message, flag) {
if (flag !== 'internal')
opts.lastPang = Date.now();
// Basically we url_encode twice server side so we can manually read the encoded version and actually do UTF-8.
// The replace for + is because FOR SOME REASON, BYOND replaces spaces with a + instead of %20, and a plus with %2b.
// Marvelous.
message = message.replace(/\+/g, "%20")
message = decoder(message)
message = byondDecode(message)
//The behemoth of filter-code (for Admin message filters)
//Note: This is proooobably hella inefficient
@@ -423,7 +437,22 @@ function ehjaxCallback(data) {
var firebugEl = document.createElement('script');
firebugEl.src = 'https://getfirebug.com/firebug-lite-debug.js';
document.body.appendChild(firebugEl);
}
} else if (data.adminMusic) {
if (typeof data.adminMusic === 'string') {
var adminMusic = byondDecode(data.adminMusic);
adminMusic = adminMusic.match(/https?:\/\/\S+/) || '';
if (data.musicRate) {
var newRate = Number(data.musicRate);
if(newRate) {
$('#adminMusic').prop('defaultPlaybackRate', newRate);
}
} else {
$('#adminMusic').prop('defaultPlaybackRate', 1.0);
}
$('#adminMusic').prop('src', adminMusic);
$('#adminMusic').trigger("play");
}
}
}
}
@@ -446,6 +475,13 @@ function toggleWasd(state) {
opts.wasd = (state == 'on' ? true : false);
}
function sendVolumeUpdate() {
opts.volumeUpdating = false;
if(opts.updatedVolume) {
runByond('?_src_=chat&proc=setMusicVolume&param[volume]='+opts.updatedVolume);
}
}
/*****************************************
*
* DOM READY
@@ -486,6 +522,7 @@ $(function() {
'spingDisabled': getCookie('pingdisabled'),
'shighlightTerms': getCookie('highlightterms'),
'shighlightColor': getCookie('highlightcolor'),
'smusicVolume': getCookie('musicVolume'),
};
if (savedConfig.sfontSize) {
@@ -517,6 +554,14 @@ $(function() {
opts.highlightColor = savedConfig.shighlightColor;
internalOutput('<span class="internal boldnshit">Loaded highlight color of: '+savedConfig.shighlightColor+'</span>', 'internal');
}
if (savedConfig.smusicVolume) {
var newVolume = clamp(savedConfig.smusicVolume, 0, 100);
$('#adminMusic').prop('volume', newVolume / 100);
$('#musicVolume').val(newVolume);
opts.updatedVolume = newVolume;
sendVolumeUpdate();
internalOutput('<span class="internal boldnshit">Loaded music volume of: '+savedConfig.smusicVolume+'</span>', 'internal');
}
(function() {
var dataCookie = getCookie('connData');
@@ -835,6 +880,26 @@ $(function() {
opts.messageCount = 0;
});
$('#musicVolumeSpan').hover(function() {
$('#musicVolumeText').addClass('hidden');
$('#musicVolume').removeClass('hidden');
}, function() {
$('#musicVolume').addClass('hidden');
$('#musicVolumeText').removeClass('hidden');
});
$('#musicVolume').change(function() {
var newVolume = $('#musicVolume').val();
newVolume = clamp(newVolume, 0, 100);
$('#adminMusic').prop('volume', newVolume / 100);
setCookie('musicVolume', newVolume, 365);
opts.updatedVolume = newVolume;
if(!opts.volumeUpdating) {
setTimeout(sendVolumeUpdate, opts.volumeUpdateDelay);
opts.volumeUpdating = true;
}
});
$('img.icon').error(iconError);

View File

@@ -120,17 +120,15 @@
else
..()
/obj/structure/holohoop/CanPass(atom/movable/mover, turf/target)
if (isitem(mover) && mover.throwing)
var/obj/item/I = mover
if(istype(I, /obj/item/projectile))
return
/obj/structure/holohoop/hitby(atom/movable/AM)
if (isitem(AM) && !istype(AM,/obj/item/projectile))
if(prob(50))
I.forceMove(get_turf(src))
visible_message("<span class='warning'>Swish! [I] lands in [src].</span>")
AM.forceMove(get_turf(src))
visible_message("<span class='warning'>Swish! [AM] lands in [src].</span>")
return
else
visible_message("<span class='danger'>[I] bounces off of [src]'s rim!</span>")
return 0
visible_message("<span class='danger'>[AM] bounces off of [src]'s rim!</span>")
return ..()
else
return ..()

View File

@@ -248,7 +248,7 @@
if(istype(src, /obj/machinery/hydroponics/soil))
add_atom_colour(rgb(255, 175, 0), FIXED_COLOUR_PRIORITY)
else
overlays += mutable_appearance('icons/obj/hydroponics/equipment.dmi', "gaia_blessing")
add_overlay(mutable_appearance('icons/obj/hydroponics/equipment.dmi', "gaia_blessing"))
set_light(3)
update_icon_hoses()

View File

@@ -643,6 +643,7 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy)
update_hair()
/mob/living/carbon/human/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_THREE)
for(var/obj/item/hand in held_items)
if(prob(current_size * 5) && hand.w_class >= ((11-current_size)/2) && dropItemToGround(hand))
@@ -651,7 +652,6 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy)
rad_act(current_size * 3)
if(mob_negates_gravity())
return
..()
/mob/living/carbon/human/proc/do_cpr(mob/living/carbon/C)
CHECK_DNA_AND_SPECIES(C)

View File

@@ -690,6 +690,7 @@
who.equip_to_slot(what, where, TRUE)
/mob/living/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_SIX)
throw_at(S,14,3, spin=1)
else

View File

@@ -15,6 +15,10 @@
/mob/living/simple_animal/drone/verb/toggle_light()
set category = "Drone"
set name = "Toggle drone light"
if(stat == DEAD)
to_chat(src, "<span class='warning'>There's no light in your life... by that I mean you're dead.</span>")
return
if(light_on)
set_light(0)
else

View File

@@ -24,11 +24,17 @@
butcher_results = list()
gold_core_spawnable = 2
/mob/living/simple_animal/pet/penguin/emperor/shamebrero
name = "Shamebrero penguin."
desc = "Shameful of all he surveys."
icon_state = "penguin_shamebrero"
icon_living = "penguin_shamebrero"
/mob/living/simple_animal/pet/penguin/baby
speak = list("gah", "noot noot", "noot!", "noot", "squeee!", "noo!")
name = "Penguin chick"
real_name = "penguin"
desc = "Can't fly and can barely waddles, but the prince of all chicks."
desc = "Can't fly and barely waddles, yet the prince of all chicks."
icon_state = "penguin_baby"
icon_living = "penguin_baby"
icon_dead = "penguin_baby_dead"

View File

@@ -28,6 +28,9 @@
var/ranged_message = "fires" //Fluff text for ranged mobs
var/ranged_cooldown = 0 //What the current cooldown on ranged attacks is, generally world.time + ranged_cooldown_time
var/ranged_cooldown_time = 30 //How long, in deciseconds, the cooldown of ranged attacks is
var/ranged_telegraph = "prepares to fire at *TARGET*!" //A message shown when the mob prepares to fire; use *TARGET* if you want to show the target's name
var/ranged_telegraph_sound //A sound played when the mob prepares to fire
var/ranged_telegraph_time = 0 //In deciseconds, how long between the telegraph and ranged shot
var/ranged_ignores_vision = FALSE //if it'll fire ranged attacks even if it lacks vision on its target, only works with environment smash
var/check_friendly_fire = 0 // Should the ranged mob check for friendlies when shooting
var/retreat_distance = null //If our mob runs from players when they're too close, set in tile distance. By default, mobs do not retreat.
@@ -232,7 +235,14 @@
var/target_distance = get_dist(targets_from,target)
if(ranged) //We ranged? Shoot at em
if(!target.Adjacent(targets_from) && ranged_cooldown <= world.time) //But make sure they're not in range for a melee attack and our range attack is off cooldown
OpenFire(target)
if(!ranged_telegraph_time || client)
OpenFire(target)
else
if(ranged_telegraph)
visible_message("<span class='danger'>[src] [replacetext(ranged_telegraph, "*TARGET*", "[target]")]</span>")
if(ranged_telegraph_sound)
playsound(src, ranged_telegraph_sound, 75, FALSE)
addtimer(CALLBACK(src, .proc/OpenFire, target), ranged_telegraph_time)
if(!Process_Spacemove()) //Drifting
walk(src,0)
return 1

View File

@@ -14,6 +14,9 @@
ranged = 1
ranged_message = "stares"
ranged_cooldown_time = 30
ranged_telegraph = "gathers energy and stares at *TARGET*!"
ranged_telegraph_sound = 'sound/magic/magic_missile.ogg'
ranged_telegraph_time = 7
throw_message = "does nothing against the hard shell of"
vision_range = 2
speed = 3
@@ -70,9 +73,11 @@
melee_damage_lower = 15
melee_damage_upper = 15
attacktext = "impales"
ranged_telegraph = "fixates on *TARGET* as its eye shines blue!"
ranged_telegraph_sound = 'sound/magic/tail_swing.ogg'
ranged_telegraph_time = 5
a_intent = INTENT_HARM
speak_emote = list("telepathically cries")
attack_sound = 'sound/weapons/bladeslice.ogg'
stat_attack = UNCONSCIOUS
movement_type = FLYING
robust_searching = 1

View File

@@ -587,7 +587,7 @@
var/turf/T = get_turf(client.eye)
stat("Location:", COORD(T))
stat("CPU:", "[world.cpu]")
stat("Instances:", "[world.contents.len]")
stat("Instances:", "[num2text(world.contents.len, 10)]")
GLOB.stat_entry()
config.stat_entry()
stat(null)

View File

@@ -170,6 +170,7 @@ By design, d1 is the smallest direction and d2 is the highest
return 0
/obj/structure/cable/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
deconstruct()

View File

@@ -1072,7 +1072,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_desc = "Aromatic beverage served piping hot. According to folk tales it can almost wake the dead."
/datum/reagent/consumable/ethanol/hearty_punch/on_mob_life(mob/living/M)
if(M.stat == UNCONSCIOUS && M.health <= 0)
if(M.health <= 0)
M.adjustBruteLoss(-7, 0)
M.adjustFireLoss(-7, 0)
M.adjustToxLoss(-7, 0)

View File

@@ -335,6 +335,7 @@
/obj/structure/disposalpipe/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
deconstruct()

View File

@@ -58,6 +58,7 @@
return ..()
/obj/machinery/disposal/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_FIVE)
deconstruct()
@@ -335,20 +336,18 @@
eject()
. = TRUE
/obj/machinery/disposal/bin/CanPass(atom/movable/mover, turf/target)
if (isitem(mover) && mover.throwing)
var/obj/item/I = mover
if(istype(I, /obj/item/projectile))
return
/obj/machinery/disposal/bin/hitby(atom/movable/AM)
if(isitem(AM) && AM.CanEnterDisposals())
if(prob(75))
I.forceMove(src)
visible_message("<span class='notice'>[I] lands in [src].</span>")
AM.forceMove(src)
visible_message("<span class='notice'>[AM] lands in [src].</span>")
update_icon()
else
visible_message("<span class='notice'>[I] bounces off of [src]'s rim!</span>")
return 0
visible_message("<span class='notice'>[AM] bounces off of [src]'s rim!</span>")
return ..()
else
return ..(mover, target)
return ..()
/obj/machinery/disposal/bin/flush()
..()
@@ -457,12 +456,12 @@
trunk.linked = src // link the pipe trunk to self
/obj/machinery/disposal/deliveryChute/place_item_in_disposal(obj/item/I, mob/user)
if(I.disposalEnterTry())
if(I.CanEnterDisposals())
..()
flush()
/obj/machinery/disposal/deliveryChute/CollidedWith(atom/movable/AM) //Go straight into the chute
if(!AM.disposalEnterTry())
if(!AM.CanEnterDisposals())
return
switch(dir)
if(NORTH)
@@ -485,16 +484,16 @@
M.forceMove(src)
flush()
/atom/movable/proc/disposalEnterTry()
/atom/movable/proc/CanEnterDisposals()
return 1
/obj/item/projectile/disposalEnterTry()
/obj/item/projectile/CanEnterDisposals()
return
/obj/effect/disposalEnterTry()
/obj/effect/CanEnterDisposals()
return
/obj/mecha/disposalEnterTry()
/obj/mecha/CanEnterDisposals()
return
/obj/machinery/disposal/deliveryChute/newHolderDestination(obj/structure/disposalholder/H)

View File

@@ -430,3 +430,23 @@
materials = list(MAT_METAL = 500, MAT_GLASS = 500)
build_path = /obj/item/organ/liver/cybernetic/upgraded
category = list("Medical Designs")
/datum/design/cybernetic_lungs
name = "Cybernetic Lungs"
desc = "A pair of cybernetic lungs."
id = "cybernetic_lungs"
req_tech = list("biotech" = 4, "materials" = 4)
build_type = PROTOLATHE
materials = list(MAT_METAL = 500, MAT_GLASS = 500)
build_path = /obj/item/organ/lungs/cybernetic
category = list("Medical Designs")
/datum/design/cybernetic_lungs_u
name = "Upgraded Cybernetic Lungs"
desc = "A pair of upgraded cybernetic lungs."
id = "cybernetic_lungs_u"
req_tech = list("biotech" = 5, "materials" = 5, "engineering" = 5)
build_type = PROTOLATHE
materials = list(MAT_METAL = 500, MAT_GLASS = 500, MAT_SILVER = 500)
build_path = /obj/item/organ/lungs/cybernetic/upgraded
category = list("Medical Designs")

View File

@@ -46,6 +46,7 @@
desc = "The incomplete body of a golem. Add ten sheets of any mineral to finish."
var/shell_type = /obj/effect/mob_spawn/human/golem
var/has_owner = FALSE //if the resulting golem obeys someone
w_class = WEIGHT_CLASS_BULKY
/obj/item/golem_shell/attackby(obj/item/I, mob/user, params)
..()

View File

@@ -4,6 +4,10 @@ GLOBAL_PROTECT(reboot_mode)
/world/proc/RunningService()
return params[SERVICE_WORLD_PARAM]
/proc/ServiceVersion()
if(world.RunningService())
return world.params[SERVICE_VERSION_PARAM]
/world/proc/ExportService(command)
return RunningService() && shell("python code/modules/server_tools/nudge.py \"[command]\"") == 0

View File

@@ -14,7 +14,8 @@ All ShuttleMove procs go here
// Called from the new turf before anything has been moved
// Only gets called if fromShuttleMove returns true first
// returns the new move_mode (based on the old)
/turf/proc/toShuttleMove(turf/oldT, shuttle_dir, move_mode)
/turf/proc/toShuttleMove(turf/oldT, move_mode, obj/docking_port/mobile/shuttle)
var/shuttle_dir = shuttle.dir
for(var/i in contents)
var/atom/movable/thing = i
if(ismob(thing))
@@ -383,4 +384,4 @@ All ShuttleMove procs go here
/obj/effect/abstract/proximity_checker/onShuttleMove(turf/newT, turf/oldT, rotation, list/movement_force, move_dir, old_dock)
//timer so it only happens once
addtimer(CALLBACK(monitor, /datum/proximity_monitor/proc/SetRange, monitor.current_range, TRUE), 0, TIMER_UNIQUE)
addtimer(CALLBACK(monitor, /datum/proximity_monitor/proc/SetRange, monitor.current_range, TRUE), 0, TIMER_UNIQUE)

View File

@@ -570,7 +570,7 @@
move_mode = moving_atom.beforeShuttleMove(newT, rotation, move_mode) //atoms
move_mode = oldT.fromShuttleMove(newT, underlying_turf_type, baseturf_cache, move_mode) //turfs
move_mode = newT.toShuttleMove(oldT, dir, move_mode) //turfs
move_mode = newT.toShuttleMove(oldT, move_mode , src) //turfs
if(move_mode & MOVE_AREA)
areas_to_move[old_area] = TRUE

View File

@@ -215,23 +215,37 @@
/obj/effect/forcefield/luxury_shuttle
var/threshold = 500
var/static/list/approved_passengers = list()
var/static/list/check_times = list()
/obj/effect/forcefield/luxury_shuttle/CanPass(atom/movable/mover, turf/target)
if(mover in approved_passengers)
return 1
return TRUE
if(!isliving(mover)) //No stowaways
return 0
return FALSE
return FALSE
#define LUXURY_MESSAGE_COOLDOWN 100
/obj/effect/forcefield/luxury_shuttle/CollidedWith(atom/movable/AM)
if(!isliving(AM))
return ..()
if(check_times[AM] && check_times[AM] > world.time) //Let's not spam the message
return ..()
check_times[AM] = world.time + LUXURY_MESSAGE_COOLDOWN
var/total_cash = 0
var/list/counted_money = list()
for(var/obj/item/coin/C in mover.GetAllContents())
for(var/obj/item/coin/C in AM.GetAllContents())
total_cash += C.value
counted_money += C
if(total_cash >= threshold)
break
for(var/obj/item/stack/spacecash/S in mover.GetAllContents())
for(var/obj/item/stack/spacecash/S in AM.GetAllContents())
total_cash += S.value * S.amount
counted_money += S
if(total_cash >= threshold)
@@ -241,12 +255,13 @@
for(var/obj/I in counted_money)
qdel(I)
to_chat(mover, "Thank you for your payment! Please enjoy your flight.")
approved_passengers += mover
return 1
to_chat(AM, "Thank you for your payment! Please enjoy your flight.")
approved_passengers += AM
check_times -= AM
return
else
to_chat(mover, "You don't have enough money to enter the main shuttle. You'll have to fly coach.")
return 0
to_chat(AM, "<span class='warning'>You don't have enough money to enter the main shuttle. You'll have to fly coach.</span>")
return ..()
/mob/living/simple_animal/hostile/bear/fightpit
name = "fight pit bear"

View File

@@ -316,6 +316,29 @@
safe_toxins_min = 16 //We breath THIS!
safe_toxins_max = 0
/obj/item/organ/lungs/cybernetic
name = "cybernetic lungs"
desc = "A cybernetic version of the lungs found in traditional humanoid entities. It functions the same as an organic lung and is merely meant as a replacement."
icon_state = "lungs-c"
origin_tech = "biotech=4"
/obj/item/organ/lungs/cybernetic/emp_act()
owner.losebreath = 20
/obj/item/organ/lungs/cybernetic/upgraded
name = "upgraded cybernetic lungs"
desc = "A more advanced version of the stock cybernetic lungs. They are capable of filtering out lower levels of toxins and carbon-dioxide."
icon_state = "lungs-c-u"
origin_tech = "biotech=5"
safe_toxins_max = 20
safe_co2_max = 20
cold_level_1_threshold = 200
cold_level_2_threshold = 140
cold_level_3_threshold = 100
#undef HUMAN_MAX_OXYLOSS
#undef HUMAN_CRIT_MAX_OXYLOSS
#undef HEAT_GAS_DAMAGE_LEVEL_1

View File

@@ -47,6 +47,7 @@
say_mod = "hisses"
taste_sensitivity = 10 // combined nose + tongue, extra sensitive
/*
/obj/item/organ/tongue/lizard/TongueSpeech(var/message)
var/regex/lizard_hiss = new("s+", "g")
var/regex/lizard_hiSS = new("S+", "g")
@@ -54,6 +55,7 @@
message = lizard_hiss.Replace(message, "sss")
message = lizard_hiSS.Replace(message, "SSS")
return message
*/
/obj/item/organ/tongue/fly
name = "proboscis"

View File

@@ -191,6 +191,14 @@ CHECK_RANDOMIZER
## Ban appeals URL - usually for a forum or wherever people should go to contact your admins.
# BANAPPEALS http://justanotherday.example.com
## System command that invokes youtube-dl, used by Play Internet Sound.
## You can install youtube-dl with
## "pip install youtube-dl" if you have pip installed
## from https://github.com/rg3/youtube-dl/releases
## or your package manager
## The default value assumes youtube-dl is in your system PATH
# INVOKE_YOUTUBEDL youtube-dl
## In-game features
##Toggle for having jobs load up from the .txt
# LOAD_JOBS_FROM_TXT

View File

@@ -0,0 +1,4 @@
author: "Jay"
delete-after: True
changes:
- tweak: "Removes the lisp lizards have when talking"

View File

@@ -0,0 +1,4 @@
author: "Xhuis"
delete-after: True
changes:
- imageadd: "The hand drill and jaws of life now have sprites on the toolbelt."

View File

@@ -0,0 +1,4 @@
author: "CitadelStationBot"
delete-after: True
changes:
- rscadd: "Admins may now show the variables interface to players to help contributors debug their new additions"

View File

@@ -0,0 +1,4 @@
author: "Naksu"
delete-after: True
changes:
- rscadd: "Added TGUI interfaces to various smartfridges of different kinds, drying racks and the disk compartmentalizer"

View File

@@ -0,0 +1,4 @@
author: "JJRcop"
delete-after: True
changes:
- rscadd: "Admins can now play media content from the web to players."

View File

@@ -0,0 +1,4 @@
author: "basilman"
delete-after: True
changes:
- rscadd: "penguins may now have shamebreros, noot noot"

View File

@@ -0,0 +1,4 @@
author: "Xhuis"
delete-after: True
changes:
- balance: "Watchers now have a half-second telegraph between charging and firing their freezing blasts."

View File

@@ -0,0 +1,4 @@
author: "Robustin"
delete-after: True
changes:
- tweak: "Golem shells no longer fit in standard crew bags."

View File

@@ -0,0 +1,4 @@
author: "CitadelStationBot"
delete-after: True
changes:
- bugfix: "Windoors open when emagged again"

View File

@@ -0,0 +1,4 @@
author: "VexingRaven"
delete-after: True
changes:
- bugfix: "Hearty Punch once again pulls people out of crit."

View File

@@ -0,0 +1,4 @@
author: "Firecage"
delete-after: True
changes:
- rscadd: "The NanoTrasen Department for Cybernetics (NDC) would like to announce the creation of Cybernetic Lungs. Both a stock variety to replace traditional stock human lungs in emergencies, and a more enhanced variety allowing greater tolerance of breathing cold air, toxins, and CO2."

View File

@@ -0,0 +1,4 @@
author: "Xhuis"
delete-after: True
changes:
- rscadd: "You can now use metal rods and departmental jumpsuits to craft departments for each banner."

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

View File

@@ -95,6 +95,7 @@
#include "code\__HELPERS\pronouns.dm"
#include "code\__HELPERS\radio.dm"
#include "code\__HELPERS\sanitize_values.dm"
#include "code\__HELPERS\shell.dm"
#include "code\__HELPERS\text.dm"
#include "code\__HELPERS\text_vr.dm"
#include "code\__HELPERS\time.dm"
@@ -281,6 +282,7 @@
#include "code\datums\antagonists\datum_traitor.dm"
#include "code\datums\antagonists\devil.dm"
#include "code\datums\antagonists\ninja.dm"
#include "code\datums\components\archaeology.dm"
#include "code\datums\components\component.dm"
#include "code\datums\components\slippery.dm"
#include "code\datums\diseases\_disease.dm"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
<ui-display title='Storage' button>
{{#partial button}}
{{#if data.isdryer}}<ui-button icon='{{data.drying ? "stop" : "tint"}}' action='Dry'>{{data.drying ? 'Stop drying' : 'Dry'}}</ui-button>{{/if}}
{{/partial}}
{{#if data.contents.length == 0}}
<ui-notice>
<span>Unfortunately, this {{data.name}} is empty.</span>
</ui-notice>
{{else}}
<div class="display tabular">
<section class="candystripe">
<section class="cell bold">
Item
</section>
<section class="cell bold">
Quantity
</section>
<section class="cell bold" align='center'>
{{#if data.verb}}{{data.verb}}{{else}}Dispense{{/if}}
</section>
</section>
{{#each data.contents}}
<section class="candystripe">
<section class="cell">
{{name}}
</section>
<section class="cell" align='right'>
{{amount}}
</section>
<section class="table" alight='right'>
<section class="cell"></section>
<section class="cell">
<ui-button grid action='Release' state={{(amount >= 1) ? null : 'disabled'}} params='{ "name" : {{name}}, "amount" : 1 }' >
One
</ui-button>
</section>
<section class="cell">
<ui-button grid action='Release' state={{(amount > 1) ? null : 'disabled'}} params='{ "name" : {{name}} }' >
Many
</ui-button>
</section>
</section>
</section>
{{/each}}
</div>
{{/if}}
</ui-display>

View File

@@ -23,6 +23,12 @@
//This is required as otherwise somebody could trick the script into leaking the api key.
$hookSecret = '08ajh0qj93209qj90jfq932j32r';
$trackPRBalance = true; //set this to false to disable PR balance tracking
$prBalanceJson = ''; //Set this to the path you'd like the writable pr balance file to be stored, not setting it writes it to the working directory
$startingPRBalance = 3; //Starting balance for never before seen users
//team 133041: tgstation/commit-access
$maintainer_team_id = 133041; //org team id that is exempt from PR balance system, setting this to null will use anyone with write access to the repo. Get from https://api.github.com/orgs/:org/teams
//Api key for pushing changelogs.
$apiKey = '209ab8d879c0f987d06a09b9d879c0f987d06a09b9d8787d0a089c';
@@ -168,7 +174,7 @@ function tag_pr($payload, $opened) {
if(strpos($lowertitle, '[wip]') !== FALSE)
$tags[] = 'Work In Progress';
$url = $payload['pull_request']['base']['repo']['url'] . '/issues/' . $payload['pull_request']['number'] . '/labels';
$url = $payload['pull_request']['issue_url'] . '/labels';
$existing_labels = file_get_contents($url, false, stream_context_create($scontext));
$existing_labels = json_decode($existing_labels, true);
@@ -195,6 +201,12 @@ function handle_pr($payload) {
switch ($payload["action"]) {
case 'opened':
tag_pr($payload, true);
if(get_pr_code_friendliness($payload) < 0){
$balances = pr_balances();
$author = $payload['pull_request']['user']['login'];
if(isset($balances[$author]) && $balances[$author] < 0)
create_comment($payload, 'You currently have a negative Fix/Feature pull request delta of ' . $balances[$author] . '. Maintainers may close this PR at will. Fixing issues or improving the codebase will improve this score.');
}
break;
case 'edited':
case 'synchronize':
@@ -210,6 +222,8 @@ function handle_pr($payload) {
else {
$action = 'merged';
checkchangelog($payload, true, true);
update_pr_balance($payload);
$validated = TRUE; //pr merged events always get announced.
}
break;
default:
@@ -223,7 +237,116 @@ function handle_pr($payload) {
$msg = '['.$payload['pull_request']['base']['repo']['full_name'].'] Pull Request '.$action.' by '.htmlSpecialChars($payload['sender']['login']).': <a href="'.$payload['pull_request']['html_url'].'">'.htmlSpecialChars('#'.$payload['pull_request']['number'].' '.$payload['pull_request']['user']['login'].' - '.$payload['pull_request']['title']).'</a>';
sendtoallservers('?announce='.urlencode($msg), $payload);
}
//creates a comment on the payload issue
function create_comment($payload, $comment){
apisend($payload['pull_request']['comments_url'], 'POST', json_encode(array('body' => $comment)));
}
//returns the payload issue's labels as a flat array
function get_pr_labels_array($payload){
$url = $payload['pull_request']['issue_url'] . '/labels';
$issue = json_decode(apisend($url), true);
$result = array();
foreach($issue as $l)
$result[] = $l['name'];
return $result;
}
//helper for getting the path the the balance json file
function pr_balance_json_path(){
global $prBalanceJson;
return $prBalanceJson != '' ? $prBalanceJson : 'pr_balances.json';
}
//return the assoc array of login -> balance for prs
function pr_balances(){
$path = pr_balance_json_path();
if(file_exists($path))
return json_decode(file_get_contents($path), true);
else
return array();
}
//returns the difference in PR balance a pull request would cause
function get_pr_code_friendliness($payload, $oldbalance = null){
global $startingPRBalance;
if($oldbalance == null)
$oldbalance = $startingPRBalance;
$labels = get_pr_labels_array($payload);
//anything not in this list defaults to 0
$label_values = array(
'Fix' => 2,
'Refactor' => 2,
'Code Improvement' => 1,
'Priority: High' => 4,
'Priority: CRITICAL' => 5,
'Atmospherics' => 4,
'Logging' => 1,
'Feedback' => 1,
'Performance' => 3,
'Feature' => -1,
'Balance/Rebalance' => -1,
'Tweak' => -1,
'PRB: Reset' => $startingPRBalance - $oldbalance,
);
$affecting = 0;
$is_neutral = FALSE;
$found_something_positive = false;
foreach($labels as $l){
if($l == 'PRB: No Update') { //no effect on balance
$affecting = 0;
break;
}
else if(isset($label_values[$l])) {
$friendliness = $label_values[$l];
if($friendliness > 0)
$found_something_positive = true;
$affecting = $found_something_positive ? max($affecting, $friendliness) : $friendliness;
}
}
return $affecting;
}
function is_maintainer($payload, $author){
global $maintainer_team_id;
$repo_is_org = $payload['pull_request']['base']['repo']['owner']['type'] == 'Organization';
if($maintainer_team_id == null || !$repo_is_org) {
$collaburl = $payload['pull_request']['base']['repo']['collaborators_url'] . '/' . $author . '/permissions';
$perms = json_decode(apisend($collaburl), true);
$permlevel = $perms['permission'];
return $permlevel == 'admin' || $permlevel == 'write';
}
else {
$check_url = 'https://api.github.com/teams/' . $maintainer_team_id . '/memberships/' . $author;
$result = json_decode(apisend($check_url), true);
return isset($result['state']); //this field won't be here if they aren't a member
}
}
//payload is a merged pull request, updates the pr balances file with the correct positive or negative balance based on comments
function update_pr_balance($payload) {
global $startingPRBalance;
global $trackPRBalance;
if(!$trackPRBalance)
return;
$author = $payload['pull_request']['user']['login'];
if(is_maintainer($payload, $author)) //immune
return;
$balances = pr_balances();
if(!isset($balances[$author]))
$balances[$author] = $startingPRBalance;
$friendliness = get_pr_code_friendliness($payload, $balances[$author]);
$balances[$author] += $friendliness;
if($balances[$author] < 0 && $friendliness < 0)
create_comment($payload, 'Your Fix/Feature pull request delta is currently below zero (' . $balances[$author] . '). Maintainers may close future Feature/Tweak/Balance PRs. Fixing issues or helping to improve the codebase will raise this score.');
else if($balances[$author] >= 0 && ($balances[$author] - $friendliness) < 0)
create_comment($payload, 'Your Fix/Feature pull request delta is now above zero (' . $balances[$author] . '). Feel free to make Feature/Tweak/Balance PRs.');
$balances_file = fopen(pr_balance_json_path(), 'w');
fwrite($balances_file, json_encode($balances));
fclose($balances_file);
}
function has_tree_been_edited($payload, $tree){