Converts the APC console from html to TGUI (#51973)

* Updates APC control panel to tgui

* Finishes things up, adds emag behavior fully

* Commented code bad

* Removes tooltips

* That shound  just be name

* Rebuild tgui

* removes header and adds a class
This commit is contained in:
LemonInTheDark
2020-07-05 08:00:26 +03:00
committed by GitHub
parent d5164f1480
commit 0b2b2bb235
4 changed files with 456 additions and 161 deletions
+153 -157
View File
@@ -3,19 +3,21 @@
desc = "Used to remotely control the flow of power to different parts of the station."
icon_screen = "solar"
icon_keyboard = "power_key"
req_access = list(ACCESS_ENGINE)
req_access = list(ACCESS_CE)
circuit = /obj/item/circuitboard/computer/apc_control
light_color = LIGHT_COLOR_YELLOW
var/mob/living/operator //Who's operating the computer right now
var/obj/machinery/power/apc/active_apc //The APC we're using right now
var/list/result_filters //For sorting the results
var/checking_logs = 0
var/should_log = TRUE
var/restoring = FALSE
var/list/logs
var/auth_id = "\[NULL\]"
var/auth_id = "\[NULL\]:"
ui_x = 550
ui_y = 500
/obj/machinery/computer/apc_control/Initialize()
/obj/machinery/computer/apc_control/Initialize(mapload, obj/item/circuitboard/C)
. = ..()
result_filters = list("Name" = null, "Charge Above" = null, "Charge Below" = null, "Responsive" = null)
logs = list()
/obj/machinery/computer/apc_control/process()
if(operator && (!operator.Adjacent(src) || machine_stat))
@@ -34,170 +36,164 @@
if(!IsAdminGhost(user))
to_chat(user,"<span class='warning'>[src] does not support AI control.</span>") //You already have APC access, cheater!
return
..(user)
..()
/obj/machinery/computer/apc_control/proc/check_apc(obj/machinery/power/apc/APC)
return APC.z == z && !APC.malfhack && !APC.aidisabled && !(APC.obj_flags & EMAGGED) && !APC.machine_stat && !istype(APC.area, /area/ai_monitored) && !APC.area.outdoors
/obj/machinery/computer/apc_control/ui_interact(mob/living/user)
. = ..()
var/dat
if(authenticated)
if(!checking_logs)
dat += "Logged in as [auth_id].<br><br>"
dat += "<i>Filters</i><br>"
dat += "<b>Name:</b> <a href='?src=[REF(src)];name_filter=1'>[result_filters["Name"] ? result_filters["Name"] : "None set"]</a><br>"
dat += "<b>Charge:</b> <a href='?src=[REF(src)];above_filter=1'>\>[result_filters["Charge Above"] ? result_filters["Charge Above"] : "NaN"]%</a> and <a href='?src=[REF(src)];below_filter=1'>\<[result_filters["Charge Below"] ? result_filters["Charge Below"] : "NaN"]%</a><br>"
dat += "<b>Accessible:</b> <a href='?src=[REF(src)];access_filter=1'>[result_filters["Responsive"] ? "Non-Responsive Only" : "All"]</a><br><br>"
for(var/A in GLOB.apcs_list)
if(check_apc(A))
var/obj/machinery/power/apc/APC = A
if(result_filters["Name"] && !findtext(APC.name, result_filters["Name"]) && !findtext(APC.area.name, result_filters["Name"]))
continue
if(result_filters["Charge Above"] && (!APC.cell || (APC.cell && (APC.cell.charge / APC.cell.maxcharge) < result_filters["Charge Above"] / 100)))
continue
if(result_filters["Charge Below"] && APC.cell && (APC.cell.charge / APC.cell.maxcharge) > result_filters["Charge Below"] / 100)
continue
if(result_filters["Responsive"] && !APC.aidisabled)
continue
dat += "<a href='?src=[REF(src)];access_apc=[REF(APC)]'>[A]</a><br>\
<b>Charge:</b> [APC.cell ? "[DisplayEnergy(APC.cell.charge)] / [DisplayEnergy(APC.cell.maxcharge)] ([round((APC.cell.charge / APC.cell.maxcharge) * 100)]%)" : "No Power cell Installed"]<br>\
<b>Area:</b> [APC.area]<br>\
[APC.aidisabled || APC.panel_open ? "<font color='#FF0000'>APC does not respond to interface query.</font>" : "<font color='#00FF00'>APC responds to interface query.</font>"]<br><br>"
dat += "<a href='?src=[REF(src)];check_logs=1'>Check Logs</a><br>"
dat += "<a href='?src=[REF(src)];log_out=1'>Log Out</a><br>"
if(obj_flags & EMAGGED)
dat += "<font color='#FF0000'>WARNING: Logging functionality partially disabled from outside source.</font><br>"
dat += "<a href='?src=[REF(src)];restore_logging=1'>Restore logging functionality?</a><br>"
else
if(logs.len)
for(var/entry in logs)
dat += "[entry]<br>"
else
dat += "<i>No activity has been recorded at this time.</i><br>"
if(obj_flags & EMAGGED)
dat += "<a href='?src=[REF(src)];clear_logs=1'><font color='#FF0000'>@#%! CLEAR LOGS</a>"
dat += "<a href='?src=[REF(src)];check_apcs=1'>Return</a>"
operator = user
else
dat = "<a href='?src=[REF(src)];authenticate=1'>Please swipe a valid ID to log in...</a>"
var/datum/browser/popup = new(user, "apc_control", name, 600, 400)
popup.set_content(dat)
popup.set_title_image(user.browse_rsc_icon(icon, icon_state))
popup.open()
/obj/machinery/computer/apc_control/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) // Remember to use the appropriate state.
operator = user
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
ui = new(user, src, ui_key, "ApcControl", "APC Controller", ui_x, ui_y, master_ui, state)
ui.open()
/obj/machinery/computer/apc_control/Topic(href, href_list)
/obj/machinery/computer/apc_control/ui_data(mob/user)
var/list/data = list()
data["auth_id"] = auth_id
data["authenticated"] = authenticated
data["emagged"] = obj_flags & EMAGGED
data["logging"] = should_log
data["restoring"] = restoring
data["logs"] = list()
data["apcs"] = list()
for(var/entry in logs)
data["logs"] += list(list("entry" = entry))
for(var/apc in GLOB.apcs_list)
if(check_apc(apc))
var/obj/machinery/power/apc/A = apc
var/has_cell = (A.cell) ? TRUE : FALSE
data["apcs"] += list(list(
"name" = A.area.name,
"operating" = A.operating,
"charge" = (has_cell) ? A.cell.percent() : "NOCELL",
"load" = DisplayPower(A.lastused_total),
"charging" = A.charging,
"chargeMode" = A.chargemode,
"eqp" = A.equipment,
"lgt" = A.lighting,
"env" = A.environ,
"responds" = A.aidisabled || A.panel_open,
"ref" = REF(A)
)
)
return data
/obj/machinery/computer/apc_control/ui_act(action, params)
if(..())
return
if(!usr || !usr.canUseTopic(src, !issilicon(usr)) || machine_stat || QDELETED(src))
return
if(href_list["authenticate"])
var/obj/item/card/id/ID = usr.get_idcard(TRUE)
if(ID && istype(ID))
if(check_access(ID))
switch(action)
if("log-in")
if(obj_flags & EMAGGED)
authenticated = TRUE
auth_id = "[ID.registered_name] ([ID.assignment])"
log_activity("logged in")
playsound(src, 'sound/machines/terminal_on.ogg', 50, FALSE)
if(href_list["log_out"])
log_activity("logged out")
playsound(src, 'sound/machines/terminal_off.ogg', 50, FALSE)
authenticated = FALSE
auth_id = "\[NULL\]"
if(href_list["restore_logging"])
to_chat(usr, "<span class='robot notice'>[icon2html(src, usr)] Logging functionality restored from backup data.</span>")
obj_flags &= ~EMAGGED
LAZYADD(logs, "<b>-=- Logging restored to full functionality at this point -=-</b>")
if(href_list["access_apc"])
playsound(src, "terminal_type", 50, FALSE)
var/obj/machinery/power/apc/APC = locate(href_list["access_apc"]) in GLOB.apcs_list
if(!APC || APC.aidisabled || APC.panel_open || QDELETED(APC))
to_chat(usr, "<span class='robot danger'>[icon2html(src, usr)] APC does not return interface request. Remote access may be disabled.</span>")
return
if(active_apc)
to_chat(usr, "<span class='robot danger'>[icon2html(src, usr)] Disconnected from [active_apc].</span>")
active_apc.say("Remote access canceled. Interface locked.")
playsound(active_apc, 'sound/machines/boltsdown.ogg', 25, FALSE)
playsound(active_apc, 'sound/machines/terminal_alert.ogg', 50, FALSE)
active_apc.locked = TRUE
active_apc.update_icon()
active_apc.remote_control = null
active_apc = null
to_chat(usr, "<span class='robot notice'>[icon2html(src, usr)] Connected to APC in [get_area_name(APC.area, TRUE)]. Interface request sent.</span>")
log_activity("remotely accessed APC in [get_area_name(APC.area, TRUE)]")
APC.remote_control = src
APC.ui_interact(usr)
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
message_admins("[ADMIN_LOOKUPFLW(usr)] remotely accessed [APC] from [src] at [AREACOORD(src)].")
log_game("[key_name(usr)] remotely accessed [APC] from [src] at [AREACOORD(src)].")
if(APC.locked)
APC.say("Remote access detected. Interface unlocked.")
playsound(APC, 'sound/machines/boltsup.ogg', 25, FALSE)
playsound(APC, 'sound/machines/terminal_alert.ogg', 50, FALSE)
APC.locked = FALSE
APC.update_icon()
active_apc = APC
if(href_list["name_filter"])
playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE)
var/new_filter = stripped_input(usr, "What name are you looking for?", name)
if(!src || !usr || !usr.canUseTopic(src, !issilicon(usr)) || machine_stat || QDELETED(src))
return
log_activity("changed name filter to \"[new_filter]\"")
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
result_filters["Name"] = new_filter
if(href_list["above_filter"])
playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE)
var/new_filter = input(usr, "Enter a percentage from 1-100 to sort by (greater than).", name) as null|num
if(!src || !usr || !usr.canUseTopic(src, !issilicon(usr)) || machine_stat || QDELETED(src))
return
log_activity("changed greater than charge filter to \"[new_filter]\"")
if(new_filter)
new_filter = clamp(new_filter, 0, 100)
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
result_filters["Charge Above"] = new_filter
if(href_list["below_filter"])
playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE)
var/new_filter = input(usr, "Enter a percentage from 1-100 to sort by (lesser than).", name) as null|num
if(!src || !usr || !usr.canUseTopic(src, !issilicon(usr)) || machine_stat || QDELETED(src))
return
log_activity("changed lesser than charge filter to \"[new_filter]\"")
if(new_filter)
new_filter = clamp(new_filter, 0, 100)
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
result_filters["Charge Below"] = new_filter
if(href_list["access_filter"])
if(isnull(result_filters["Responsive"]))
result_filters["Responsive"] = 1
log_activity("sorted by non-responsive APCs only")
else
result_filters["Responsive"] = !result_filters["Responsive"]
log_activity("sorted by all APCs")
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
if(href_list["check_logs"])
checking_logs = TRUE
log_activity("checked logs")
if(href_list["check_apcs"])
checking_logs = FALSE
log_activity("checked APCs")
if(href_list["clear_logs"])
logs = list()
ui_interact(usr) //Refresh the UI after a filter changes
auth_id = "Unknown (Unknown):"
log_activity("[auth_id] logged in to the terminal")
return
var/obj/item/card/id/ID = operator.get_idcard(TRUE)
if(ID && istype(ID))
if(check_access(ID))
authenticated = TRUE
auth_id = "[ID.registered_name] ([ID.assignment]):"
log_activity("[auth_id] logged in to the terminal")
playsound(src, 'sound/machines/terminal_on.ogg', 50, FALSE)
else
auth_id = "[ID.registered_name] ([ID.assignment]):"
log_activity("[auth_id] attempted to log into the terminal")
return
auth_id = "Unknown (Unknown):"
log_activity("[auth_id] attempted to log into the terminal")
if("log-out")
log_activity("[auth_id] logged out of the terminal")
playsound(src, 'sound/machines/terminal_off.ogg', 50, FALSE)
authenticated = FALSE
auth_id = "\[NULL\]"
if("toggle-logs")
should_log = !should_log
log_game("[key_name(operator)] set the logs of [src] in [AREACOORD(src)] [should_log ? "On" : "Off"]")
if("restore-console")
restoring = TRUE
addtimer(CALLBACK(src, .proc/restore_comp), rand(3,5) * 9)
if("access-apc")
var/ref = params["ref"]
playsound(src, "terminal_type", 50, FALSE)
var/obj/machinery/power/apc/APC = locate(ref) in GLOB.apcs_list
if(!APC)
return
if(active_apc)
to_chat(operator, "<span class='robot danger'>[icon2html(src, auth_id)] Disconnected from [active_apc].</span>")
active_apc.say("Remote access canceled. Interface locked.")
playsound(active_apc, 'sound/machines/boltsdown.ogg', 25, FALSE)
playsound(active_apc, 'sound/machines/terminal_alert.ogg', 50, FALSE)
active_apc.locked = TRUE
active_apc.update_icon()
active_apc.remote_control = null
active_apc = null
APC.remote_control = src
APC.ui_interact(operator)
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
log_game("[key_name(operator)] remotely accessed [APC] from [src] at [AREACOORD(src)].")
log_activity("[auth_id] remotely accessed APC in [get_area_name(APC.area, TRUE)]")
if(APC.locked)
APC.say("Remote access detected. Interface unlocked.")
playsound(APC, 'sound/machines/boltsup.ogg', 25, FALSE)
playsound(APC, 'sound/machines/terminal_alert.ogg', 50, FALSE)
APC.locked = FALSE
APC.update_icon()
active_apc = APC
if("check-logs")
log_activity("Checked Logs")
if("check-apcs")
log_activity("Checked APCs")
if("toggle-minor")
var/ref = params["ref"]
var/type = params["type"]
var/value = params["value"]
var/obj/machinery/power/apc/target = locate(ref) in GLOB.apcs_list
if(!target)
return
target.vars[type] = target.setsubsystem(text2num(value))
target.update_icon()
target.update()
var/setTo = ""
switch(target.vars[type])
if(0)
setTo = "Off"
if(1)
setTo = "Auto Off"
if(2)
setTo = "On"
if(3)
setTo = "Auto On"
log_activity("Set APC [target.area.name] [type] to [setTo]")
log_game("[key_name(operator)] Set APC [target.area.name] [type] to [setTo]]")
if("breaker")
var/ref = params["ref"]
var/obj/machinery/power/apc/target = locate(ref) in GLOB.apcs_list
target.toggle_breaker()
var/setTo = target.operating ? "On" : "Off"
log_activity("Turned APC [target.area.name]'s breaker [setTo]")
/obj/machinery/computer/apc_control/emag_act(mob/user)
if(!authenticated)
to_chat(user, "<span class='warning'>You bypass [src]'s access requirements using your emag.</span>")
authenticated = TRUE
log_activity("logged in")
else if(!(obj_flags & EMAGGED))
if(user)
user.visible_message("<span class='warning'>You emag [src], disabling precise logging and allowing you to clear logs.</span>")
log_game("[key_name(user)] emagged [src] at [AREACOORD(src)], disabling operator tracking.")
obj_flags |= EMAGGED
if(obj_flags & EMAGGED)
return
obj_flags |= EMAGGED
log_game("[key_name(user)] emagged [src] at [AREACOORD(src)]")
playsound(src, "sparks", 50, TRUE)
/obj/machinery/computer/apc_control/proc/log_activity(log_text)
var/op_string = operator && !(obj_flags & EMAGGED) ? operator : "\[NULL OPERATOR\]"
LAZYADD(logs, "<b>([station_time_timestamp()])</b> [op_string] [log_text]")
if(!should_log)
return
LAZYADD(logs, "([station_time_timestamp()]): [auth_id] [log_text]")
/obj/machinery/computer/apc_control/proc/restore_comp()
obj_flags &= ~EMAGGED
should_log = TRUE
log_game("[key_name(operator)] restored the logs of [src] in [AREACOORD(src)]")
log_activity("-=- Logging restored to full functionality at this point -=-")
restoring = FALSE
/mob/proc/using_power_flow_console()
for(var/obj/machinery/computer/apc_control/A in range(1, src))
+300
View File
@@ -0,0 +1,300 @@
import { map, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { pureComponentHooks } from 'common/react';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Dimmer, Flex, Icon, Table, Tabs } from '../components';
import { Fragment, Window } from '../layouts';
import { AreaCharge, powerRank } from './PowerMonitor';
export const ApcControl = (props, context) => {
const { data } = useBackend(context);
return (
<Window resizable>
{data.authenticated === 1
&& <ApcLoggedIn />}
{data.authenticated === 0
&& <ApcLoggedOut />}
</Window>
);
};
const ApcLoggedOut = (props, context) => {
const { act, data } = useBackend(context);
const { emagged } = data;
const text = emagged === 1 ? 'Open' : 'Log In';
return (
<Window.Content>
<Button
fluid
color={emagged === 1 ? '' : 'good'}
content={text}
onClick={() => act('log-in')} />
</Window.Content>
);
};
const ApcLoggedIn = (props, context) => {
const { act, data } = useBackend(context);
const { restoring } = data;
const [
tabIndex,
setTabIndex,
] = useLocalState(context, 'tab-index', 1);
return (
<Fragment>
<Tabs>
<Tabs.Tab
selected={tabIndex === 1}
onClick={() => {
setTabIndex(1);
act('check-apcs');
}}>
APC Control Panel
</Tabs.Tab>
<Tabs.Tab
selected={tabIndex === 2}
onClick={() => {
setTabIndex(2);
act('check-logs');
}}>
Log View Panel
</Tabs.Tab>
</Tabs>
{restoring === 1 && (
<Dimmer fontSize="32px">
<Icon name="cog" spin />
{' Resetting...'}
</Dimmer>
)}
{tabIndex === 1 && (
<Fragment>
<ControlPanel />
<Box fillPositionedParent top="53px">
<Window.Content scrollable>
<ApcControlScene />
</Window.Content>
</Box>
</Fragment>
)}
{tabIndex === 2 && (
<Box fillPositionedParent top="20px">
<Window.Content scrollable>
<LogPanel />
</Window.Content>
</Box>
)}
</Fragment>
);
};
const ControlPanel = (props, context) => {
const { act, data } = useBackend(context);
const {
emagged,
logging,
} = data;
const [
sortByField,
setSortByField,
] = useLocalState(context, 'sortByField', null);
return (
<Flex>
<Flex.Item>
<Box inline mr={2} color="label">
Sort by:
</Box>
<Button.Checkbox
checked={sortByField === 'name'}
content="Name"
onClick={() => setSortByField(sortByField !== 'name' && 'name')} />
<Button.Checkbox
checked={sortByField === 'charge'}
content="Charge"
onClick={() => setSortByField(
sortByField !== 'charge' && 'charge'
)} />
<Button.Checkbox
checked={sortByField === 'draw'}
content="Draw"
onClick={() => setSortByField(sortByField !== 'draw' && 'draw')} />
</Flex.Item>
<Flex.Item grow={1} />
<Flex.Item>
{emagged === 1 && (
<Fragment>
<Button
color={logging === 1 ? 'bad' : 'good'}
content={logging === 1 ? 'Stop Logging' : 'Restore Logging'}
onClick={() => act('toggle-logs')}
/>
<Button
content="Reset Console"
onClick={() => act('restore-console')}
/>
</Fragment>
)}
<Button
color="bad"
content="Log Out"
onClick={() => act('log-out')}
/>
</Flex.Item>
</Flex>
);
};
const ApcControlScene = (props, context) => {
const { data, act } = useBackend(context);
const [
sortByField,
] = useLocalState(context, 'sortByField', null);
const apcs = flow([
map((apc, i) => ({
...apc,
// Generate a unique id
id: apc.name + i,
})),
sortByField === 'name' && sortBy(apc => apc.name),
sortByField === 'charge' && sortBy(apc => -apc.charge),
sortByField === 'draw' && sortBy(
apc => -powerRank(apc.load),
apc => -parseFloat(apc.load)),
])(data.apcs);
return (
<Table>
<Table.Row header>
<Table.Cell>
On/Off
</Table.Cell>
<Table.Cell>
Area
</Table.Cell>
<Table.Cell collapsing>
Charge
</Table.Cell>
<Table.Cell collapsing textAlign="right">
Draw
</Table.Cell>
<Table.Cell collapsing title="Equipment">
Eqp
</Table.Cell>
<Table.Cell collapsing title="Lighting">
Lgt
</Table.Cell>
<Table.Cell collapsing title="Environment">
Env
</Table.Cell>
</Table.Row>
{apcs.map((apc, i) => (
<tr
key={apc.id}
className="Table__row candystripe">
<td>
<Button
icon={apc.operating ? 'power-off' : 'times'}
color={apc.operating ? 'good' : 'bad'}
onClick={() => act('breaker', {
ref: apc.ref,
})}
/>
</td>
<td>
<Button
onClick={() => act('access-apc', {
ref: apc.ref,
})}>
{apc.name}
</Button>
</td>
<td className="Table__cell text-right text-nowrap">
<AreaCharge
charging={apc.charging}
charge={apc.charge}
/>
</td>
<td className="Table__cell text-right text-nowrap">
{apc.load}
</td>
<td className="Table__cell text-center text-nowrap">
<AreaStatusColorButton
target="equipment"
status={apc.eqp}
apc={apc}
act={act}
/>
</td>
<td className="Table__cell text-center text-nowrap">
<AreaStatusColorButton
target="lighting"
status={apc.lgt}
apc={apc}
act={act}
/>
</td>
<td className="Table__cell text-center text-nowrap">
<AreaStatusColorButton
target="environ"
status={apc.env}
apc={apc}
act={act}
/>
</td>
</tr>
))}
</Table>
);
};
const LogPanel = (props, context) => {
const { data } = useBackend(context);
const logs = flow([
map((line, i) => ({
...line,
// Generate a unique id
id: line.entry + i,
})),
logs => logs.reverse(),
])(data.logs);
return (
<Box m={-0.5}>
{logs.map(line => (
<Box
p={0.5}
key={line.id}
className="candystripe"
bold>
{line.entry}
</Box>
))}
</Box>
);
};
const AreaStatusColorButton = props => {
const { target, status, apc, act } = props;
const power = Boolean(status & 2);
const mode = Boolean(status & 1);
return (
<Button
icon={mode ? 'sync' : 'power-off'}
color={power ? 'good' : 'bad'}
onClick={() => act('toggle-minor', {
type: target,
value: statusChange(status),
ref: apc.ref,
})}
/>
);
};
const statusChange = status => {
// mode flip power flip both flip
// 0, 2, 3
return status === 0 ? 2 : status === 2 ? 3 : 0;
};
AreaStatusColorButton.defaultHooks = pureComponentHooks;
@@ -6,10 +6,9 @@ import { Component, Fragment } from 'inferno';
import { Box, Button, Chart, ColorBox, Flex, Icon, LabeledList, ProgressBar, Section, Table } from '../components';
import { Window } from '../layouts';
import { useBackend, useLocalState } from '../backend';
const PEAK_DRAW = 500000;
const powerRank = str => {
export const powerRank = str => {
const unit = String(str.split(' ')[1]).toLowerCase();
return ['w', 'kw', 'mw', 'gw'].indexOf(unit);
};
@@ -172,7 +171,7 @@ export const PowerMonitorContent = (props, context) => {
);
};
const AreaCharge = props => {
export const AreaCharge = props => {
const { charging, charge } = props;
return (
<Fragment>
File diff suppressed because one or more lines are too long