Canister and paper updates

should fix papah exploits, though some runtiming is occuring
This commit is contained in:
Artur
2020-07-04 16:14:42 +03:00
parent 81354e9aee
commit a304ed7f49
9 changed files with 894 additions and 259 deletions

View File

@@ -118,6 +118,9 @@
//reusing the PDA option because I really don't think news comments are worth a config option
WRITE_LOG(GLOB.world_pda_log, "COMMENT: [text]")
/proc/log_paper(text)
WRITE_LOG(GLOB.world_paper_log, "PAPER: [text]")
/proc/log_telecomms(text)
if (CONFIG_GET(flag/log_telecomms))
WRITE_LOG(GLOB.world_telecomms_log, "TCOMMS: [text]")

View File

@@ -32,6 +32,8 @@ GLOBAL_VAR(world_asset_log)
GLOBAL_PROTECT(world_asset_log)
GLOBAL_VAR(world_map_error_log)
GLOBAL_PROTECT(world_map_error_log)
GLOBAL_VAR(world_paper_log)
GLOBAL_PROTECT(world_paper_log)
GLOBAL_VAR(subsystem_log)
GLOBAL_PROTECT(subsystem_log)
GLOBAL_VAR(reagent_log)

View File

@@ -132,6 +132,7 @@ GLOBAL_LIST(topic_status_cache)
GLOB.world_runtime_log = "[GLOB.log_directory]/runtime.log"
GLOB.query_debug_log = "[GLOB.log_directory]/query_debug.log"
GLOB.world_job_debug_log = "[GLOB.log_directory]/job_debug.log"
GLOB.world_paper_log = "[GLOB.log_directory]/paper.log"
GLOB.tgui_log = "[GLOB.log_directory]/tgui.log"
GLOB.subsystem_log = "[GLOB.log_directory]/subsystem.log"
GLOB.reagent_log = "[GLOB.log_directory]/reagents.log"

View File

@@ -5,6 +5,8 @@
desc = "A canister for the storage of gas."
icon_state = "yellow"
density = TRUE
ui_x = 300
ui_y = 232
var/valve_open = FALSE
var/obj/machinery/atmospherics/components/binary/passive_gate/pump
@@ -34,6 +36,7 @@
var/restricted = FALSE
req_access = list()
var/update = 0
var/static/list/label2types = list(
"n2" = /obj/machinery/portable_atmospherics/canister/nitrogen,
"o2" = /obj/machinery/portable_atmospherics/canister/oxygen,
@@ -159,11 +162,11 @@
/obj/machinery/portable_atmospherics/canister/proto
name = "prototype canister"
/obj/machinery/portable_atmospherics/canister/proto/default
name = "prototype canister"
desc = "The best way to fix an atmospheric emergency... or the best way to introduce one."
icon_state = "proto"
icon_state = "proto"
volume = 5000
max_integrity = 300
temperature_resistance = 2000 + T0C
@@ -171,6 +174,7 @@
can_min_release_pressure = (ONE_ATMOSPHERE / 30)
prototype = TRUE
/obj/machinery/portable_atmospherics/canister/proto/default/oxygen
name = "prototype canister"
desc = "A prototype canister for a prototype bike, what could go wrong?"
@@ -192,6 +196,7 @@
update_icon()
/obj/machinery/portable_atmospherics/canister/Destroy()
qdel(pump)
pump = null
@@ -215,7 +220,6 @@
/obj/machinery/portable_atmospherics/canister/update_overlays()
. = ..()
if(holding)
. += "can-open"
if(connected_port)
@@ -245,7 +249,8 @@
new /obj/item/stack/sheet/metal (loc, 5)
qdel(src)
/obj/machinery/portable_atmospherics/canister/welder_act(mob/living/user, obj/item/I)
obj/machinery/portable_atmospherics/canister/welder_act(mob/living/user, obj/item/I)
..()
if(user.a_intent == INTENT_HARM)
return FALSE
@@ -261,7 +266,8 @@
return TRUE
/obj/machinery/portable_atmospherics/canister/obj_break(damage_flag)
if((stat & BROKEN) || (flags_1 & NODECONSTRUCT_1))
. = ..()
if(!.)
return
canister_break()
@@ -272,10 +278,9 @@
T.assume_air(expelled_gas)
air_update_turf()
stat |= BROKEN
obj_break()
density = FALSE
playsound(src.loc, 'sound/effects/spray.ogg', 10, 1, -3)
update_icon()
playsound(src.loc, 'sound/effects/spray.ogg', 10, TRUE, -3)
investigate_log("was destroyed.", INVESTIGATE_ATMOS)
if(holding)
@@ -318,7 +323,7 @@
datum/tgui/master_ui = null, datum/ui_state/state = GLOB.physical_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
ui = new(user, src, ui_key, "Canister", name, 420, 405, master_ui, state)
ui = new(user, src, ui_key, "Canister", name, ui_x, ui_y, master_ui, state)
ui.open()
/obj/machinery/portable_atmospherics/canister/ui_data()
@@ -353,7 +358,7 @@
return
switch(action)
if("relabel")
var/label = input("New canister label:", name) as null|anything in label2types
var/label = input("New canister label:", name) as null|anything in sortList(label2types)
if(label && !..())
var/newtype = label2types[label]
if(newtype)

View File

@@ -4,12 +4,19 @@
*
* lipstick wiping is in code/game/objects/items/weapons/cosmetics.dm!
*/
#define MAX_PAPER_LENGTH 5000
#define MAX_PAPER_STAMPS 30 // Too low?
#define MAX_PAPER_STAMPS_OVERLAYS 4
#define MODE_READING 0
#define MODE_WRITING 1
#define MODE_STAMPING 2
/**
** Paper is now using markdown (like in github pull notes) for ALL rendering
** so we do loose a bit of functionality but we gain in easy of use of
** paper and getting rid of that crashing bug
**/
/obj/item/paper
var/static/regex/sign_regex = regex("%s(?:ign)?(?=\\s|$)", "igm")
name = "paper"
gender = NEUTER
icon = 'icons/obj/bureaucracy.dmi'
@@ -27,28 +34,37 @@
dog_fashion = /datum/dog_fashion/head
color = "white"
/// What's actually written on the paper.
var/info
var/info = ""
/// The (text for the) stamps on the paper.
var/stamps
var/list/stamped
var/list/stamps /// Positioning for the stamp in tgui
var/list/stamped /// Overlay info
/// This REALLY should be a componenet. Basicly used during, april fools
/// to honk at you
var/rigged = 0
var/spam_flag = 0
///
var/contact_poison // Reagent ID to transfer on contact
var/contact_poison_volume = 0
var/ui_x = 600
var/ui_y = 800
/// When a piece of paper cannot be edited, this makes it mutable
var/finalized = FALSE
/// We MAY be edited, mabye we are just looking at it or something.
var/readonly = FALSE
/// Color of the pin that wrote on this paper
/// When the sheet can be "filled out"
/// This is an associated list
var/list/form_fields = null
var/field_counter = 1
/// What edit mode we are in and who is
/// writing on it right now
var/edit_mode = MODE_READING
var/mob/living/edit_usr = null
/// Setup for writing to a sheet
var/pen_color = "black"
var/pen_font = ""
var/is_crayon = FALSE
/// Setup for stamping a sheet
var/obj/item/stamp/current_stamp = null
var/stamp_class = null
/**
** This proc copies this sheet of paper to a new
@@ -58,12 +74,12 @@
/obj/item/paper/proc/copy()
var/obj/item/paper/N = new(arglist(args))
N.info = info
N.pen_color = pen_color
N.color = color
N.finalized = TRUE
N.update_icon_state()
N.stamps = stamps
N.stamped = stamped.Copy()
N.form_fields = form_fields.Copy()
N.field_counter = field_counter
copy_overlays(N, TRUE)
return N
@@ -72,9 +88,10 @@
** icons. You can modify the pen_color after if need
** be.
**/
/obj/item/paper/proc/setText(text, read_only = TRUE)
readonly = read_only
/obj/item/paper/proc/setText(text)
info = text
form_fields = null
field_counter = 0
update_icon_state()
/obj/item/paper/pickup(user)
@@ -108,6 +125,7 @@
var/datum/asset/spritesheet/assets = get_asset_datum(/datum/asset/spritesheet/simple/paper)
. = replacetext(html, "<!--customheadhtml-->", assets.css_tag())
/obj/item/paper/verb/rename()
set name = "Rename paper"
set category = "Object"
@@ -132,13 +150,17 @@
user.visible_message("<span class='suicide'>[user] scratches a grid on [user.p_their()] wrist with the paper! It looks like [user.p_theyre()] trying to commit sudoku...</span>")
return (BRUTELOSS)
/// ONLY USED FOR APRIL FOOLS
/// ONLY USED FOR APRIL FOOLS
/obj/item/paper/proc/reset_spamflag()
spam_flag = FALSE
/obj/item/paper/attack_self(mob/user)
readonly = TRUE /// Assume we are just reading it
if(edit_usr == user)
// we are shifting out of editing mode
edit_mode = MODE_READING
edit_usr = null
if(rigged && (SSevents.holidays && SSevents.holidays[APRIL_FOOLS]))
if(!spam_flag)
spam_flag = TRUE
@@ -148,8 +170,7 @@
/obj/item/paper/proc/clearpaper()
finalized = FALSE
info = null
info = ""
stamps = null
LAZYCLEARLIST(stamped)
cut_overlays()
@@ -159,49 +180,56 @@
/obj/item/paper/can_interact(mob/user)
if(!..())
return FALSE
if(resistance_flags & ON_FIRE) /// Are we on fire? Hard ot read if so
if(resistance_flags & ON_FIRE) // Are we on fire? Hard ot read if so
return FALSE
if(user.is_blind()) /// Even harder to read if your blind...braile? humm
if(user.is_blind()) // Even harder to read if your blind...braile? humm
return FALSE
return user.can_read(src) // checks if the user can read.
/obj/item/paper/attackby(obj/item/P, mob/living/carbon/human/user, params)
readonly = TRUE /// Assume we are just reading it
if(istype(P, /obj/item/pen) || istype(P, /obj/item/toy/crayon))
if(finalized)
to_chat(user, "<span class='warning'>This sheet of paper has already been written too!</span>")
if(length(info) >= MAX_PAPER_LENGTH) // Sheet must have less than 1000 charaters
to_chat(user, "<span class='warning'>This sheet of paper is full!</span>")
return
readonly = FALSE /// Nope we are going to write stuff
/// should a crayon be in the same subtype as a pen? How about a brush or charcoal?
if(istype(P, /obj/item/pen))
var/obj/item/pen/PEN = P
pen_color = PEN.colour
else
if(edit_mode != MODE_READING)
to_chat(user, "<span class='warning'>[edit_usr] is already working on this sheet!</span>")
return
edit_mode = MODE_WRITING
edit_usr = user
// should a crayon be in the same subtype as a pen? How about a brush or charcoal?
// TODO: Convert all writing stuff to one type, /obj/item/art_tool maybe?
is_crayon = istype(P, /obj/item/toy/crayon);
if(is_crayon)
var/obj/item/toy/crayon/PEN = P
pen_color = PEN.crayon_color
pen_font = CRAYON_FONT
pen_color = PEN.paint_color
else
var/obj/item/pen/PEN = P
pen_font = PEN.font
pen_color = PEN.colour
ui_interact(user)
return
else if(istype(P, /obj/item/stamp))
if(!in_range(src, user))
if(edit_mode != MODE_READING)
to_chat(user, "<span class='warning'>[edit_usr] is already working on this sheet!</span>")
return
edit_mode = MODE_STAMPING // we are read only becausse the sheet is full
edit_usr = user
current_stamp = P
var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/simple/paper)
if(isnull(stamps))
stamps = sheet.css_tag()
stamps += sheet.icon_tag(P.icon_state)
var/mutable_appearance/stampoverlay = mutable_appearance('icons/obj/bureaucracy.dmi', "paper_[P.icon_state]")
stampoverlay.pixel_x = rand(-2, 2)
stampoverlay.pixel_y = rand(-3, 2)
stamp_class = sheet.icon_class_name(P.icon_state)
LAZYADD(stamped, P.icon_state)
add_overlay(stampoverlay)
to_chat(user, "<span class='notice'>You stamp the paper with your rubber stamp.</span>")
to_chat(user, "<span class='notice'>You ready your stamp over the paper! </span>")
ui_interact(user)
return /// Normaly you just stamp, you don't need to read the thing
if(P.get_temperature())
else if(P.get_temperature())
if(HAS_TRAIT(user, TRAIT_CLUMSY) && prob(10))
user.visible_message("<span class='warning'>[user] accidentally ignites [user.p_them()]self!</span>", \
"<span class='userdanger'>You miss the paper and accidentally light yourself on fire!</span>")
@@ -210,15 +238,19 @@
user.IgniteMob()
return
if(!(in_range(user, src))) //to prevent issues as a result of telepathically lighting a paper
return
user.dropItemToGround(src)
user.visible_message("<span class='danger'>[user] lights [src] ablaze with [P]!</span>", "<span class='danger'>You light [src] on fire!</span>")
fire_act()
else
if(edit_mode != MODE_READING)
to_chat(user, "You look at the sheet while [edit_usr] edits it")
else
edit_mode = MODE_READING
ui_interact(user) // The other ui will be created with just read mode outside of this
. = ..()
/obj/item/paper/fire_act(exposed_temperature, exposed_volume)
..()
if(!(resistance_flags & FIRE_PROOF))
@@ -235,25 +267,58 @@
if(!ui)
var/datum/asset/assets = get_asset_datum(/datum/asset/spritesheet/simple/paper)
assets.send(user)
/// The x size is because we double the width for the editor
ui = new(user, src, ui_key, "PaperSheet", name, 400, 600, master_ui, state)
// The x size is because we double the width for the editor
ui = new(user, src, ui_key, "PaperSheet", name, ui_x, ui_y, master_ui, state)
ui.set_autoupdate(FALSE)
ui.open()
/obj/item/paper/ui_close(mob/user)
/// close the editing window and change the mode
if(edit_usr != null && user == edit_usr)
edit_mode = MODE_READING
edit_usr = null
current_stamp = null
stamp_class = null
. = ..()
/obj/item/paper/proc/ui_force_close()
var/datum/tgui/ui = SStgui.try_update_ui(usr, src, "main");
if(ui)
ui.close()
/obj/item/paper/proc/ui_update()
var/datum/tgui/ui = SStgui.try_update_ui(usr, src, "main");
if(ui)
ui.update()
/obj/item/paper/ui_data(mob/user)
var/list/data = list()
// Should all this go in static data and just do a forced update?
data["text"] = info
data["paper_state"] = icon_state /// TODO: show the sheet will bloodied or crinkling
data["pen_color"] = pen_color
data["paper_color"] = color || "white" // color might not be set
data["edit_sheet"] = readonly || finalized ? FALSE : TRUE
/// data["stamps_info"] = list(stamp_info)
data["max_length"] = MAX_PAPER_LENGTH
data["paper_state"] = icon_state /// TODO: show the sheet will bloodied or crinkling?
data["paper_color"] = !color || color == "white" ? "#FFFFFF" : color // color might not be set
data["stamps"] = stamps
if(edit_usr == null || user != edit_usr)
data["edit_mode"] = MODE_READING /// Eveyone else is just an observer
else
data["edit_mode"] = edit_mode
// pen info for editing
data["is_crayon"] = is_crayon
data["pen_font"] = pen_font
data["pen_color"] = pen_color
// stamping info for..stamping
data["stamp_class"] = stamp_class
data["field_counter"] = field_counter
return data
@@ -261,19 +326,73 @@
if(..())
return
switch(action)
if("stamp")
var/stamp_x = text2num(params["x"])
var/stamp_y = text2num(params["y"])
var/stamp_r = text2num(params["r"]) // rotation in degrees
if (isnull(stamps))
stamps = new/list()
if(stamps.len < MAX_PAPER_STAMPS)
// I hate byond when dealing with freaking lists
stamps += list(list(stamp_class, stamp_x, stamp_y,stamp_r)) /// WHHHHY
/// This does the overlay stuff
if (isnull(stamped))
stamped = new/list()
if(stamped.len < MAX_PAPER_STAMPS_OVERLAYS)
var/mutable_appearance/stampoverlay = mutable_appearance('icons/obj/bureaucracy.dmi', "paper_[current_stamp.icon_state]")
stampoverlay.pixel_x = rand(-2, 2)
stampoverlay.pixel_y = rand(-3, 2)
add_overlay(stampoverlay)
LAZYADD(stamped, current_stamp.icon_state)
edit_usr.visible_message("<span class='notice'>[edit_usr] stamps [src] with [current_stamp]!</span>", "<span class='notice'>You stamp [src] with [current_stamp]!</span>")
else
to_chat(usr, pick("You try to stamp but you miss!", "There is no where else you can stamp!"))
ui_update()
. = TRUE
if("save")
var/in_paper = params["text"]
if(length(in_paper) > 0 && length(in_paper) < 1000) // Sheet must have less than 1000 charaters
info = in_paper
finalized = TRUE // once you have writen to a sheet you cannot write again
to_chat(usr, "You have finished your paper masterpiece!");
ui_update()
else
var/paper_len = length(in_paper)
var/list/fields = params["form_fields"]
field_counter = params["field_counter"] ? text2num(params["field_counter"]) : field_counter
if(paper_len > MAX_PAPER_LENGTH)
// Side note, the only way we should get here is if
// the javascript was modified, somehow, outside of
// byond.
log_paper("[key_name(edit_usr)] writing to paper [name], and overwrote it by [paper_len-MAX_PAPER_LENGTH], aborting")
ui_force_close()
else if(paper_len == 0)
to_chat(usr, pick("Writing block strikes again!", "You forgot to write anthing!"))
ui_update()
ui_force_close()
else
// Next find the sign marker and replace it with somones sig
// All other processing should of been done in the js module
in_paper = sign_regex.Replace(in_paper, "<font face=\"[SIGNFONT]\"><i>[edit_usr]</i></font>")
// Do the same with form fields
log_paper("[key_name(edit_usr)] writing to paper [name]")
if(info != in_paper)
to_chat(usr, "You have added to your paper masterpiece!");
info = in_paper
if(!fields) //Possible runtiming
return
if(fields && fields.len > 0)
for(var/key in fields) // In case somone %sign in a field
form_fields[key] = sign_regex.Replace(fields[key], "<font face=\"[SIGNFONT]\"><i>[edit_usr]</i></font>")
/// Switch ui to reading mode
edit_mode = MODE_READING
edit_usr = null
ui_update()
update_icon()
. = TRUE
/*
* Construction paper
*/
@@ -305,3 +424,10 @@
/obj/item/paper/crumpled/muddy
icon_state = "scrap_mud"
#undef MAX_PAPER_LENGTH
#undef MAX_PAPER_STAMPS
#undef MAX_PAPER_STAMPS_OVERLAYS
#undef MODE_READING
#undef MODE_WRITING
#undef MODE_STAMPING

View File

@@ -1,5 +1,8 @@
import { toFixed } from 'common/math';
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { AnimatedNumber, Box, Button, Flex, Knob, LabeledList, NoticeBox, Section } from '../components';
import { AnimatedNumber, Box, Button, Icon, Knob, LabeledControls, LabeledList, Section, Tooltip } from '../components';
import { formatSiUnit } from '../format';
import { Window } from '../layouts';
export const Canister = (props, context) => {
@@ -20,82 +23,105 @@ export const Canister = (props, context) => {
return (
<Window>
<Window.Content>
<NoticeBox>
The regulator {hasHoldingTank ? 'is' : 'is not'} connected
to a tank.
</NoticeBox>
<Section
title="Canister"
buttons={(
<Button
icon="pencil-alt"
content="Relabel"
onClick={() => act('relabel')} />
<Fragment>
{!!isPrototype && (
<Button
mr={1}
icon={restricted ? 'lock' : 'unlock'}
color="caution"
content={restricted
? 'Engineering'
: 'Public'}
onClick={() => act('restricted')} />
)}
<Button
icon="pencil-alt"
content="Relabel"
onClick={() => act('relabel')} />
</Fragment>
)}>
<Flex mx={-1}>
<Flex.Item
mx={1}
align="center"
textAlign="center">
<Knob
size={2}
color={!!valveOpen && 'yellow'}
value={releasePressure}
unit="kPa"
minValue={minReleasePressure}
maxValue={maxReleasePressure}
step={5}
stepPixelSize={1}
onDrag={(e, value) => act('pressure', {
pressure: value,
})} />
</Flex.Item>
<Flex.Item
mx={1}
my={-0.5}
align="center"
textAlign="center">
<Box my={0.5} color="label">
Valve
</Box>
<Box my={0.5} width="60px">
<AnimatedNumber value={releasePressure} /> kPa
<LabeledControls>
<LabeledControls.Item
minWidth="66px"
label="Pressure">
<AnimatedNumber
value={tankPressure}
format={value => {
if (value < 10000) {
return toFixed(value) + ' kPa';
}
return formatSiUnit(value * 1000, 1, 'Pa');
}} />
</LabeledControls.Item>
<LabeledControls.Item label="Regulator">
<Box
position="relative"
left="-8px">
<Knob
size={1.25}
color={!!valveOpen && 'yellow'}
value={releasePressure}
unit="kPa"
minValue={minReleasePressure}
maxValue={maxReleasePressure}
step={5}
stepPixelSize={1}
onDrag={(e, value) => act('pressure', {
pressure: value,
})} />
<Button
fluid
position="absolute"
top="-2px"
right="-20px"
color="transparent"
icon="fast-forward"
onClick={() => act('pressure', {
pressure: maxReleasePressure,
})} />
<Button
fluid
position="absolute"
top="16px"
right="-20px"
color="transparent"
icon="undo"
onClick={() => act('pressure', {
pressure: defaultReleasePressure,
})} />
</Box>
</LabeledControls.Item>
<LabeledControls.Item label="Valve">
<Button
my={0.5}
width="50px"
lineHeight={2}
fontSize="11px"
color={valveOpen
? (hasHoldingTank ? 'caution' : 'danger')
: null}
content={valveOpen ? 'Open' : 'Closed'}
onClick={() => act('valve')} />
</Flex.Item>
<Flex.Item
mx={1}
grow={1}
basis={0}>
<LabeledList>
<LabeledList.Item label="Pressure">
<AnimatedNumber value={tankPressure} /> kPa
</LabeledList.Item>
<LabeledList.Item
label="Port"
color={portConnected ? 'good' : 'average'}>
{portConnected ? 'Connected' : 'Not Connected'}
</LabeledList.Item>
{!!isPrototype && (
<LabeledList.Item label="Access">
<Button
icon={restricted ? 'lock' : 'unlock'}
color="caution"
content={restricted
? 'Engineering'
: 'Public'}
onClick={() => act('restricted')} />
</LabeledList.Item>
)}
</LabeledList>
</Flex.Item>
</Flex>
</LabeledControls.Item>
<LabeledControls.Item
mr={1}
label="Port">
<Box position="relative">
<Icon
size={1.25}
name={portConnected ? 'plug' : 'times'}
color={portConnected ? 'good' : 'bad'} />
<Tooltip
content={portConnected
? 'Connected'
: 'Disconnected'}
position="top" />
</Box>
</LabeledControls.Item>
</LabeledControls>
</Section>
<Section
title="Holding Tank"

View File

@@ -5,33 +5,128 @@
* @author Changes stylemistake
* @license MIT
*/
import { Tabs, Box, Flex, Button, TextArea } from '../components';
import { useBackend, useSharedState, useLocalState } from '../backend';
import { Component } from 'inferno';
import { Tabs, Box, Flex, TextArea } from '../components';
import { useBackend } from '../backend';
import { Window } from '../layouts';
import marked from 'marked';
import DOMPurify from 'dompurify';
import { classes, isFalsy } from "common/react";
// There is a sanatize option in marked but they say its deprecated.
// Might as well use a proper one then
import { createLogger } from '../logging';
import { Fragment } from 'inferno';
import { vecCreate, vecAdd, vecSubtract } from 'common/vector';
const logger = createLogger('PaperSheet');
const MAX_PAPER_LENGTH = 5000; // Question, should we send this with ui_data?
const walkTokens = token => {
switch (token.type) {
case 'link':
case 'image':
token.type = 'text';
// Once asset system is up change to some default image
// or rewrite for icon images
token.href = "";
break;
}
const sanatize_text = value => {
// This is VERY important to think first if you NEED
// the tag you put in here. We are pushing all this
// though dangerouslySetInnerHTML and even though
// the default DOMPurify kills javascript, it dosn't
// kill href links or such
return DOMPurify.sanitize(value, {
FORBID_ATTR: ['class', 'style'],
ALLOWED_TAGS: [
'br', 'code', 'li', 'p', 'pre',
'span', 'table', 'td', 'tr',
'th', 'ul', 'ol', 'menu', 'font', 'b',
'center', 'table', 'tr', 'th',
],
});
};
// Hacky, yes, works?...yes
const textWidth = (text, font, fontsize) => {
// default font height is 12 in tgui
font = fontsize + "x " + font;
const c = document.createElement('canvas');
const ctx = c.getContext("2d");
ctx.font = font;
const width = ctx.measureText(text).width;
return width;
};
const setFontinText = (text, font, color, bold=false) => {
return "<span style=\""
+ "color:'" + color + "';"
+ "font-family:'" + font + "';"
+ ((bold)
? "font-weight: bold;"
: "")
+ "\">" + text + "</span>";
};
const paperfield_id_headder = "paperfield_";
const createIDHeader = index => {
return "paperfield_" + index;
};
// To make a field you do a [_______] or however long the field is
// we will then output a TEXT input for it that hopefuly covers
// the exact amount of spaces
const field_regex = /\[(_+)\]/g;
const field_tag_regex = /\[<input\s+(.*?)id="(?<id>paperfield_\d+)"(.*?)\/>\]/gm;
const sign_regex = /%s(?:ign)?(?=\\s|$)/igm;
const field_id_regex = /id\s*=\s*'(paperfield_\d+)'/g;
const field_maxlength_regex = /maxlength\s*=\s*(\d+)/g;
const createInputField = (length, width, font,
fontsize, color, id) => {
return "[<input "
+ "type=\"text\" "
+ "style=\""
+ "font:'" + fontsize + "x " + font + "';"
+ "color:'" + color + "';"
+ "min-width:" + width + ";"
+ "max-width:" + width + ";"
+ "\" "
+ "id=\"" + id + "\" "
+ "maxlength=" + length +" "
+ "size=" + length + " "
+ "/>]";
};
const createFields = (txt, font, fontsize, color, counter) => {
const ret_text = txt.replace(field_regex, (match, p1, offset, string) => {
const width = textWidth(match, font, fontsize) + "px";
return createInputField(p1.length,
width, font, fontsize, color, createIDHeader(counter++));
});
return { counter: counter, text: ret_text };
};
const signDocument = (txt, color, user) => {
return txt.replace(sign_regex, () => {
return setFontinText(user, "Times New Roman", color, true);
});
};
const run_marked_default = value => {
const clean = DOMPurify.sanitize(value);
return marked(clean,
// Override function, any links and images should
// kill any other marked tokens we don't want here
const walkTokens = token => {
switch (token.type) {
case 'url':
case 'autolink':
case 'reflink':
case 'link':
case 'image':
token.type = 'text';
// Once asset system is up change to some default image
// or rewrite for icon images
token.href = "";
break;
}
};
return marked(value,
{ breaks: true,
smartypants: true,
smartLists: true,
@@ -40,126 +135,501 @@ const run_marked_default = value => {
baseUrl: "thisshouldbreakhttp",
});
};
const fillAllfields = fields => {
for (const id in fields) {
const dom = document.getElementById(id);
if (dom) {
const PaperSheetView = (props, context) => {
const { data } = useBackend(context);
}
const dom_text = dom && dom.value ? dom.value : "";
}
};
/*
** This gets the field, and finds the dom object and sees if
** the user has typed something in. If so, it replaces,
** the dom object, in txt with the value, spaces so it
** fits the [] format and saves the value into a object
** There may be ways to optimize this in javascript but
** doing this in byond is nightmarish.
**
** It returns any values that were saved and a corrected
** html code or null if nothing was updated
*/
const checkAllFields = (txt, font, color, user_name, bold=false) => {
let matches;
let values = {};
let replace = [];
// I know its tempting to wrap ALL this in a .replace
// HOWEVER the user might not of entered anything
// if thats the case we are rebuilding the entire string
// for nothing, if nothing is entered, txt is just returned
while ((matches = field_tag_regex.exec(txt)) !== null) {
const full_match = matches[0];
const id = matches.groups.id;
if (id) {
const dom = document.getElementById(id);
// make sure we got data, and kill any html that might
// be in it
const dom_text = dom && dom.value ? dom.value : "";
if (dom_text.length === 0) { continue; }
const sanitized_text
= DOMPurify.sanitize(
dom.value.trim(), { ALLOWED_TAGS: [] });
if (sanitized_text.length === 0) { continue; }
// this is easyer than doing a bunch of text manipulations
const target = dom.cloneNode(true);
// in case they sign in a field
if (sanitized_text.match(sign_regex)) {
target.style.fontFamily = "Times New Roman";
bold=true;
target.defaultValue = user_name;
} else {
target.style.fontFamily = font;
target.defaultValue = sanitized_text;
}
if (bold) {
target.style.fontWeight = "bold";
}
target.style.color = color;
target.disabled = true;
const wrap = document.createElement('div');
wrap.appendChild(target);
values[id] = sanitized_text; // save the data
replace.push({ value: "[" + wrap.innerHTML + "]", raw_text: full_match });
}
}
if (replace.length > 0) {
for (const o of replace) {
txt = txt.replace(o.raw_text, o.value);
}
}
return { text: txt, fields: values };
};
const pauseEvent = e => {
if (e.stopPropagation) { e.stopPropagation(); }
if (e.preventDefault) { e.preventDefault(); }
e.cancelBubble=true;
e.returnValue=false;
return false;
};
const Stamp = (props, context) => {
const {
paper_color = "white",
pen_color = "black",
text = '',
} = data;
const {
value = text || '',
image,
opacity,
...rest
} = props;
// We use this for caching so we don't keep refreshing it each time
const [marked_text, setMarkedText] = useLocalState(context, 'marked_text',
{ __html: run_marked_default(value) });
const matrix_trasform = 'rotate(' + image.rotate
+ 'deg) translate(' + image.x + 'px,' + image.y + 'px)';
const stamp_trasform = {
'transform': matrix_trasform,
'-ms-transform': matrix_trasform,
'-webkit-transform': matrix_trasform,
'opacity': opacity || 1.0,
'position': 'absolute',
};
return (
<Box
opacity={1}
backgroundColor={paper_color}
color={pen_color}
{...rest}
dangerouslySetInnerHTML={marked_text} />
<div
className={classes([
'paper121x54',
image.sprite,
])}
style={stamp_trasform}
/>
);
};
// If the prop dosn't exist OR its not true
const isFalsyProperty = (obj, prop) => {
return Object.prototype.hasOwnProperty.call(obj, prop) && !isFalsy(obj.prop);
};
const setInputReadonly = (text, readonly) => {
return readonly
? text.replace(/<input\s[^d]/g, "<input disabled ")
: text.replace(/<input\sdisabled\s/g, "<input ");
};
// got to make this a full component if we
// want to control updates
const PaperSheetView = (props, context) => {
const {
value,
stamps,
backgroundColor,
readOnly,
...rest
} = props;
const readonly = !isFalsy(readOnly);
const stamp_list = stamps || [];
const text_html = { __html: "<span class='paper-text'>"
+ setInputReadonly(value, readonly) + "</span>" };
return (
<Box position="relative"
backgroundColor={backgroundColor} width="100%" height="100%" >
<Box fillPositionedParent={1} width="100%" height="100%"
dangerouslySetInnerHTML={text_html} p="10px" />
{stamp_list.map((o, i) => (
<Stamp key={o[0] + i}
image={{ sprite: o[0], x: o[1], y: o[2], rotate: o[3] }} />
))}
</Box>
);
};
const PaperSheetEdit = (props, context) => {
const { act, data } = useBackend(context);
const [text, setText] = useLocalState(context, 'text', data.text || '');
const [marked_text, setMarkedText] = useLocalState(context, 'marked_text',
{ __html: run_marked_default(text) });
const [
previewSelected,
setPreviewSelected,
] = useLocalState(context, 'preview', "Preview");
const {
paper_color = "white",
pen_color = "black",
} = data;
const onInputHandler = (e, value) => {
if (value.length < 1000) {
setText(value);
setMarkedText({ __html: run_marked_default(value) });
} else {
setText(value.substr(1000));
}
};
// again, need the states for dragging and such
class PaperSheetStamper extends Component {
constructor(props, context) {
super(props, context);
this.state = {
x: 0,
y: 0,
rotate: 0,
};
}
findStampPosition(e) {
const position = {
x: event.pageX,
y: event.pageY,
};
return (
<Flex direction="column">
<Flex.Item>
<Tabs>
<Tabs.Tab
key="marked_edit"
textColor={'black'}
backgroundColor={previewSelected === "Edit" ? "grey" : "white"}
selected={previewSelected === "Edit"}
onClick={() => setPreviewSelected("Edit")}>
Edit
</Tabs.Tab>
<Tabs.Tab
key="marked_preview"
textColor={'black'}
backgroundColor={previewSelected === "Preview" ? "grey" : "white"}
selected={previewSelected === "Preview"}
onClick={() => setPreviewSelected("Preview")}>
Preview
</Tabs.Tab>
<Tabs.Tab
key="marked_done"
textColor={'black'}
backgroundColor={previewSelected === "confirm"
? "red"
: previewSelected === "save"
const offset = {
left: e.target.offsetLeft,
top: e.target.offsetTop,
};
let reference = e.target.offsetParent;
while (reference) {
offset.left += reference.offsetLeft;
offset.top += reference.offsetTop;
reference = reference.offsetParent;
}
const pos_x = position.x - offset.left;
const pos_y = position.y - offset.top;
const pos = vecCreate(pos_x, pos_y);
const center_offset = vecCreate((121/2), (51/2));
const center = vecSubtract(pos, center_offset);
return center;
}
componentDidMount() {
document.onwheel = this.handleWheel.bind(this);
}
handleMouseMove(e) {
const pos = this.findStampPosition(e);
// center offset of stamp
pauseEvent(e);
this.setState({ x: pos[0], y: pos[1] });
}
handleMouseClick(e) {
const pos = this.findStampPosition(e);
const { act, data } = useBackend(this.context);
act("stamp", { x: pos[0], y: pos[1], r: this.state.rotate });
this.setState({ x: pos[0], y: pos[1] });
}
handleWheel(e) {
const rotate_amount = e.deltaY > 0 ? 15 : -15;
if (e.deltaY < 0 && this.state.rotate === 0) {
this.setState({ rotate: (360+rotate_amount) });
} else if (e.deltaY > 0 && this.state.rotate === 360) {
this.setState({ rotate: rotate_amount });
} else {
const rotate = { rotate: rotate_amount + this.state.rotate };
this.setState(() => rotate);
}
pauseEvent(e);
}
render() {
const {
value,
stamp_class,
stamps,
...rest
} = this.props;
const stamp_list = stamps || [];
const current_pos = {
sprite: stamp_class,
x: this.state.x,
y: this.state.y,
rotate: this.state.rotate,
};
return (
<Box onClick={this.handleMouseClick.bind(this)}
onMouseMove={this.handleMouseMove.bind(this)}
onwheel={this.handleWheel.bind(this)} {...rest}>
<PaperSheetView
readOnly={1}
value={value}
stamps={stamp_list} />
<Stamp
opacity={0.5} image={current_pos} />
</Box>
);
}
}
// ugh. So have to turn this into a full
// component too if I want to keep updates
// low and keep the wierd flashing down
class PaperSheetEdit extends Component {
constructor(props, context) {
super(props, context);
this.state = {
previewSelected: "Preview",
old_text: props.value || "",
textarea_text: "",
combined_text: props.value || "",
};
}
// This is the main rendering part, this creates the html from marked text
// as well as the form fields
createPreview(value, do_fields=false) {
const { data } = useBackend(this.context);
const {
text,
pen_color,
pen_font,
is_crayon,
field_counter,
edit_usr,
} = data;
const out = { text: text };
// check if we are adding to paper, if not
// we still have to check if somone entered something
// into the fields
value = value.trim();
if (value.length > 0) {
// First lets make sure it ends in a new line
value += value[value.length] === "\n" ? " \n" : "\n \n";
// Second, we sanatize the text of html
const sanatized_text = sanatize_text(value);
const signed_text = signDocument(sanatized_text, pen_color, edit_usr);
// Third we replace the [__] with fields as markedjs fucks them up
const fielded_text = createFields(signed_text
, pen_font, 12, pen_color, field_counter);
// Fourth, parse the text using markup
const formated_text = run_marked_default(fielded_text.text);
// Fifth, we wrap the created text in the pin color, and font.
// crayon is bold (<b> tags), mabye make fountain pin italic?
const fonted_text = setFontinText(formated_text
, pen_font, pen_color, is_crayon);
out.text += fonted_text;
out.field_counter = fielded_text.counter;
}
if (do_fields) {
// finaly we check all the form fields to see
// if any data was entered by the user and
// if it was return the data and modify
// the text
const final_processing = checkAllFields(out.text
, pen_font, pen_color, edit_usr, is_crayon);
out.text = final_processing.text;
out.form_fields = final_processing.fields;
}
return out;
}
onInputHandler(e, value) {
if (value !== this.state.textarea_text) {
const combined_length = this.state.old_text.length
+ this.state.textarea_text.length;
if (combined_length > MAX_PAPER_LENGTH) {
if ((combined_length - MAX_PAPER_LENGTH) >= value.length) {
value = ''; // basicly we cannot add any more text to the paper
} else {
value = value.substr(0, value.length
- (combined_length - MAX_PAPER_LENGTH));
}
// we check again to save an update
if (value === this.state.textarea_text) { return; }// do nooothing
}
this.setState(() => {
return { textarea_text: value,
combined_text: this.createPreview(value) }; });
}
}
// the final update send to byond, final upkeep
finalUpdate(new_text) {
const { act } = useBackend(this.context);
const final_processing = this.createPreview(new_text, true);
act('save', final_processing);
this.setState(() => { return {
textarea_text: "",
previewSelected: "save",
combined_text: final_processing.text,
}; });
// byond should switch us to readonly mode from here
}
render() {
const {
value="",
textColor,
fontFamily,
stamps,
backgroundColor,
...rest
} = this.props;
return (
<Flex direction="column" fillPositionedParent={1}>
<Flex.Item>
<Tabs>
<Tabs.Tab
key="marked_edit"
textColor={'black'}
backgroundColor={this.state.previewSelected === "Edit"
? "grey"
: "white"}
selected={previewSelected === "confirm"
|| previewSelected === "save"}
onClick={() => {
if (previewSelected === "confirm") {
act('save', { text: DOMPurify.sanitize(text) });
} else {
setPreviewSelected("confirm");
}
}}>
{ previewSelected === "confirm" ? "confirm" : "save" }
</Tabs.Tab>
</Tabs>
selected={this.state.previewSelected === "Edit"}
onClick={() => this.setState({ previewSelected: "Edit" })}>
Edit
</Tabs.Tab>
<Tabs.Tab
key="marked_preview"
textColor={'black'}
backgroundColor={this.state.previewSelected === "Preview"
? "grey"
: "white"}
selected={this.state.previewSelected === "Preview"}
onClick={() => this.setState(() => {
const new_state = {
previewSelected: "Preview",
textarea_text: this.state.textarea_text,
combined_text: this.createPreview(
this.state.textarea_text).text,
};
return new_state;
})}>
Preview
</Tabs.Tab>
<Tabs.Tab
key="marked_done"
textColor={'black'}
backgroundColor={this.state.previewSelected === "confirm"
? "red"
: this.state.previewSelected === "save"
? "grey"
: "white"}
selected={this.state.previewSelected === "confirm"
|| this.state.previewSelected === "save"}
onClick={() => {
if (this.state.previewSelected === "confirm") {
this.finalUpdate(this.state.textarea_text);
} else if (this.state.previewSelected === "Edit") {
this.setState(() => {
const new_state = {
previewSelected: "confirm",
textarea_text: this.state.textarea_text,
combined_text: this.createPreview(
this.state.textarea_text).text,
};
return new_state;
});
} else {
this.setState({ previewSelected: "confirm" });
}
}}>
{ this.state.previewSelected === "confirm" ? "confirm" : "save" }
</Tabs.Tab>
</Tabs>
</Flex.Item>
<Flex.Item
grow={1}
basis={1}>
{this.state.previewSelected === "Edit" && (
<TextArea
value={this.state.textarea_text}
textColor={textColor}
fontFamily={fontFamily}
height={(window.innerHeight - 80) + "px"}
backgroundColor={backgroundColor}
onInput={this.onInputHandler.bind(this)} />
) || (
<PaperSheetView
value={this.state.combined_text}
stamps={stamps}
fontFamily={fontFamily}
textColor={textColor} />
)}
</Flex.Item>
</Flex>
);
}
}
</Flex.Item>
<Flex.Item
grow={1}
basis={1}>
{previewSelected === "Edit" && (
<TextArea
value={text}
backgroundColor="white"
textColor="black"
height={(window.innerHeight - 80)+ "px"}
onInput={onInputHandler} />
) || (
<PaperSheetView />
)}
</Flex.Item>
</Flex>
);
};
export const PaperSheet = (props, context) => {
const { data } = useBackend(context);
const {
edit_sheet,
edit_mode,
text,
paper_color,
pen_color = "black",
pen_font = "Verdana",
stamps,
stamp_class,
stamped,
} = data;
// You might ask why? Because Window/window content do wierd
// css stuff with white for some reason
const backgroundColor = paper_color && paper_color !== "white"
? paper_color
: "#FFFFFF";
const background_style = {
'background-color': backgroundColor,
};
const stamp_list = !stamps || stamps === null
? []
: stamps;
const decide_mode = mode => {
switch (mode) {
case 0: // min-height="100vh" min-width="100vw"
return (<PaperSheetView
value={text}
stamps={stamp_list}
readOnly={1} />);
case 1:
return (<PaperSheetEdit value={text}
textColor={pen_color}
fontFamily={pen_font}
stamps={stamp_list}
backgroundColor={backgroundColor}
/>);
case 2:
return (<PaperSheetStamper value={text}
stamps={stamp_list}
stamp_class={stamp_class}
/>);
default:
return "ERROR ERROR WE CANNOT BE HERE!!";
}
};
return (
<Window resizable theme="paper">
<Window.Content scrollable>
{edit_sheet && (
<PaperSheetEdit />
) || (
<PaperSheetView fillPositionedParent />
)}
<Window resizable theme="paper" style={background_style}>
<Window.Content min-height="100vh" min-width="100vw"
style={background_style}>
<Box fillPositionedParent={1} min-height="100vh"
min-width="100vw" backgroundColor={backgroundColor}>
{decide_mode(edit_mode)}
</Box>
</Window.Content>
</Window>
);

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,11 @@
@use '../base.scss';
@use '../functions.scss' as *;
$text-color: #272727 !default;
$border-color: #88bfff !default;
$border-radius: base.$border-radius !default;
$color-background: #bb9b68 !default;
$color-border: #272727 !default;
.TextArea {
overflow: hidden;
@@ -17,9 +20,7 @@
border: 1px solid $border-color;
border: 1px solid rgba($border-color, 0.75);
border-radius: $border-radius;
color: #fff;
background-color: #000;
background-color: rgba(0, 0, 0, 0.75);
background-color: $color-background;
margin-right: 2px;
line-height: 17px;
overflow: visible;
@@ -28,7 +29,7 @@
box-sizing: border-box; /* Opera/IE 8+ */
width:100%;
}
.TextArea_filler {
word-wrap: break-word; /* make sure the div and the textarea wrap words in the same way */
@@ -67,9 +68,8 @@
height: 17px;
margin: 0;
padding: 0 6px;
font-family: Verdana, sans-serif;
font-family: inherit;
background-color: transparent;
color: #fff;
color: inherit;
box-sizing: border-box;
width: 100%;
@@ -91,6 +91,8 @@
&:-ms-input-placeholder {
font-style: italic;
color: #777;
color: rgba(255, 255, 255, 0.45);
/* color: rgba(255, 255, 255, 0.45);*/
}
}
}