#define INSTRUMENT_MAX_LINE_NUMBER 300 #define INSTRUMENT_MAX_LINE_LENGTH 50 /datum/song var/name = "Untitled" var/list/lines = new() var/tempo = 5 // delay between notes var/playing = 0 // if we're playing var/repeat = 0 // number of times remaining to repeat var/max_repeats = 10 // maximum times we can repeat var/instrumentDir = "piano" // the folder with the sounds var/instrumentExt = "ogg" // the file extension var/obj/instrumentObj = null // the associated obj playing the sound var/show_edithelp = TRUE // show help for editing var/show_playhelp = FALSE // show help for keyboard playback var/live_octave_base = 5 // the base octave of playing live with keyboard var/recording = FALSE // if recording keyboard notes to play back var/recorded_line = "" // if recording, put this into the last line at the end var/time_since_last_note = 0 // for chords /datum/song/New(dir, obj) tempo = sanitize_tempo(tempo) instrumentDir = dir instrumentObj = obj /datum/song/Destroy() instrumentObj = null ..() // note is a number from 1-7 for A-G // acc is either "b", "n", or "#" // oct is 1-8 (or 9 for C) /datum/song/proc/playnote(note, acc as text, oct, mob/user, live = FALSE) // handle accidental -> B<>C of E<>F if(acc == "b" && (note == 3 || note == 6)) // C or F if(note == 3) oct-- note-- acc = "n" else if(acc == "#" && (note == 2 || note == 5)) // B or E if(note == 2) oct++ note++ acc = "n" else if(acc == "#" && (note == 7)) //G# note = 1 acc = "b" else if(acc == "#") // mass convert all sharps to flats, octave jump already handled acc = "b" note++ // check octave, C is allowed to go to 9 if(oct < 1 || (note == 3 ? oct > 9 : oct > 8)) return // now generate name var/soundfile = "sound/instruments/[instrumentDir]/[ascii2text(note+64)][acc][oct].[instrumentExt]" soundfile = file(soundfile) // make sure the note exists if(!fexists(soundfile)) return // and play var/turf/source = get_turf(instrumentObj) for(var/mob/M in get_hearers_in_view(15, source)) if(!M.client) continue if(M.is_deaf()) continue if(istype(instrumentObj,/obj/item/device/instrument)) var/obj/item/device/instrument/INS = instrumentObj INS.OnPlayed(user,M) if(!M.client.prefs.get_pref(/datum/preference_setting/toggle/hear_instruments)) continue M.playsound_local(source, soundfile, 100, falloff = 5) if(recording && live) if(world.time - time_since_last_note <= 0 && recorded_line != "") recorded_line += "-" else if(recorded_line != "") recorded_line += "," if(world.time - time_since_last_note > 5) var/intervals = round((world.time - time_since_last_note)/5) - 1 if(intervals) for(var/i in 1 to intervals) recorded_line += "," recorded_line += "[ascii2text(note+64)][acc][oct]" if(world.time - time_since_last_note <= 3) recorded_line += "/2" else if(world.time - time_since_last_note == 1) recorded_line += "/4" time_since_last_note = world.time /datum/song/proc/shouldStopPlaying(mob/user) if(instrumentObj) if(!instrumentObj.Adjacent(user) || user.stat) return 1 else if(istype(instrumentObj,/obj/structure/piano)) var/obj/structure/piano/P = instrumentObj if(P.broken) return 1 return !instrumentObj.anchored // add special cases to stop in subclasses else return 1 /datum/song/proc/playsong(mob/user) while(repeat >= 0) var/cur_oct[7] var/cur_acc[7] for(var/i = 1 to 7) cur_oct[i] = 3 cur_acc[i] = "n" var/lineCount = 1; for(var/line in lines) //to_chat(world, "line: [line]") var/chordCount = 1; for(var/beat in splittext(lowertext(line), ",")) //to_chat(world, "beat: [beat] / beat length: [length(beat)]") if(!length(beat)) //This occurs when a comma is at the end of a line beat = " " //It's intended to be a space so here we make it a space var/list/notes = splittext(beat, "/") var/delta = length(notes)==2 && text2num(notes[2]) ? text2num(notes[2]) : 1 var/note_str = splittext(notes[1], "-") var/duration = sanitize_tempo(src.tempo/delta) for(var/note in note_str) //to_chat(world, "note: [note]") if(!playing || shouldStopPlaying(user))//If the instrument is playing, or special case playing = 0 return if(length(note) == 0) continue //world << "Parse: [copytext(note,1,2)]" var/cur_note = text2ascii(note) - 96 if(cur_note < 1 || cur_note > 7) continue for(var/i=2 to length(note)) var/ni = copytext(note,i,i+1) if(!text2num(ni)) if(ni == "#" || ni == "b" || ni == "n") cur_acc[cur_note] = ni else if(ni == "s") cur_acc[cur_note] = "#" // so shift is never required else cur_oct[cur_note] = text2num(ni) playnote(cur_note, cur_acc[cur_note], cur_oct[cur_note],user) var/datum/nanoui/ui = nanomanager.get_open_ui(user, src, "instrument") if (ui) ui.send_message("activeChord", list2params(list(lineCount, chordCount))) //nanomanager.send_message(src, instrumentObj.name, "activeChord", list(lineCount, chordCount)) /*if(notes.len >= 2 && text2num(notes[2])) sleep(sanitize_tempo(tempo / text2num(notes[2]))) else sleep(tempo)*/ sleep(duration) chordCount++ lineCount++ repeat-- playing = 0 repeat = 0 ui_interact(user) /datum/song/ui_interact(mob/user, ui_key="main", datum/nanoui/ui=null, var/force_open=NANOUI_FOCUS) var/data = list( "repeat" = repeat, "ticklag" = world.tick_lag, "bpm" = round(600 / tempo), "lines" = json_encode(lines), "src" = "\ref[src]", //needed to create buttons in the js "octave" = live_octave_base, "show_playhelp" = show_playhelp, "show_edithelp" = show_edithelp, "recording" = recording ) ui = nanomanager.try_update_ui(user, src, ui_key, ui, data, force_open) if (!ui) ui = new(user, src, "instrument", "instrument.tmpl", instrumentObj.name, 800, 600, nstatus_proc = /proc/nanoui_instrument_status_proc) ui.add_stylesheet("instrument.css") ui.set_initial_data(data) ui.open() //copypaste but the src_object is the instrument //constants dont work for some reason /proc/nanoui_instrument_status_proc(var/datum/nanoui/nano) var/datum/song/src_song = nano.src_object if(!istype(src_song)) return 0 var/obj/instrumentObj = src_song.instrumentObj if(!istype(instrumentObj)) return 0 var/can_interactive = 0 if(nano.user.mutations && nano.user.mutations.len) if(M_TK in nano.user.mutations) can_interactive = 1 else if(isrobot(nano.user)) if(instrumentObj in view(7, nano.user)) can_interactive = 1 else can_interactive = (isAI(nano.user) || !nano.distance_check || isAdminGhost(nano.user)) if (can_interactive) return 2 // interactive (green visibility) else var/dist = 0 if(istype(instrumentObj, /atom)) var/atom/A = instrumentObj if(isobserver(nano.user)) var/mob/dead/observer/O = nano.user var/ghost_flags = 0 if(A.ghost_write) ghost_flags |= PERMIT_ALL if(canGhostWrite(O,A,"",ghost_flags) || isAdminGhost(O)) return 2 // interactive (green visibility) else if(canGhostRead(O,A,ghost_flags)) return 1 // update only (orange visibility) dist = get_dist(instrumentObj, nano.user) if (dist > 4) return -1 if ((nano.allowed_user_stat > -1) && (nano.user.stat > nano.allowed_user_stat)) return 0 // no updates, completely disabled (red visibility) else if (nano.user.restrained() || nano.user.lying) return 1 // update only (orange visibility) else if (!(instrumentObj in view(4, nano.user))) // If the src object is not in visable, set status to 0 return 0 // no updates, completely disabled (red visibility) else if (dist <= 1) return 2 // interactive (green visibility) else if (dist <= 2) return 1 // update only (orange visibility) else if (dist <= 4) return 0 // no updates, completely disabled (red visibility) /datum/song/Topic(href, href_list) if(!instrumentObj.Adjacent(usr) || usr.stat || href_list["close"]) var/datum/nanoui/ui = nanomanager.get_open_ui(usr, src, "instrument") if (ui) ui.close() return //nanomanager.send_message(src, "instrument", "messageReceived", null, usr) instrumentObj.add_fingerprint(usr) if(href_list["newsong"]) lines = new() tempo = sanitize_tempo(5) // default 120 BPM name = "" else if(href_list["import"]) var/t = "" do t = html_encode(input(usr, "Please paste the entire song, formatted:", text("[]", name), t) as message) if(!in_range(instrumentObj, usr)) return if(length(t) >= INSTRUMENT_MAX_LINE_LENGTH*INSTRUMENT_MAX_LINE_NUMBER) var/cont = input(usr, "Your message is too long! Would you like to continue editing it?", "", "yes") in list("yes", "no") if(cont == "no") break while(length(t) > INSTRUMENT_MAX_LINE_LENGTH*INSTRUMENT_MAX_LINE_NUMBER) //split into lines spawn() lines = splittext(t, "\n") //if the user didn't paste in a song, we have nothing to do here if(lines.len == 0) alert("You can't import an empty song!") return if(copytext(lines[1],1,6) == "BPM: ") tempo = sanitize_tempo(600 / text2num(copytext(lines[1],6))) lines.Cut(1,2) else tempo = sanitize_tempo(5) // default 120 BPM if(lines.len > INSTRUMENT_MAX_LINE_NUMBER) alert(usr, "Too many lines! Cutting down...") lines.Cut(INSTRUMENT_MAX_LINE_NUMBER+1) var/linenum = 1 for(var/l in lines) if(length(l) > INSTRUMENT_MAX_LINE_LENGTH) alert(usr, "Line [linenum] too long! Removing...") lines.Remove(l) else linenum++ ui_interact(usr) // make sure updates when complete if(href_list["repeat"]) //Changing this from a toggle to a number of repeats to avoid infinite loops. if(playing) return //So that people cant keep adding to repeat. If the do it intentionally, it could result in the server crashing. repeat += round(text2num(href_list["repeat"])) if(repeat < 0) repeat = 0 if(repeat > max_repeats) repeat = max_repeats else if(href_list["tempo"]) tempo = sanitize_tempo(tempo + text2num(href_list["tempo"])) else if(href_list["play"]) if(playing) return playing = 1 spawn() playsong(usr) return //no need to reload the window else if(href_list["play_note"]) if(href_list["play_sharp"] == "s") href_list["play_sharp"] = "#" playnote(text2num(href_list["play_note"]), href_list["play_sharp"], text2num(href_list["play_oct"]), usr, TRUE) return //no need to reload the window else if(href_list["increase_octave"]) if(live_octave_base < 8) live_octave_base++ else if(href_list["decrease_octave"]) if(live_octave_base > 1) live_octave_base-- else if(href_list["toggle_playhelp"]) show_playhelp = !show_playhelp else if(href_list["toggle_edithelp"]) show_edithelp = !show_edithelp else if(href_list["toggle_recording"]) recording = !recording if(!recording && lines.len <= INSTRUMENT_MAX_LINE_NUMBER) if(length(recorded_line) > INSTRUMENT_MAX_LINE_LENGTH) recorded_line = html_encode(copytext(recorded_line, 1, INSTRUMENT_MAX_LINE_LENGTH)) lines.Add(recorded_line) else if(recording) recorded_line = "" time_since_last_note = world.time else if(href_list["newline"]) var/newline = input("Enter your line: ", instrumentObj.name) as text|null if(!newline || !in_range(instrumentObj, usr)) return if(lines.len > INSTRUMENT_MAX_LINE_NUMBER) return if(length(newline) > INSTRUMENT_MAX_LINE_LENGTH) newline = html_encode(copytext(newline, 1, INSTRUMENT_MAX_LINE_LENGTH)) lines.Add(newline) else if(href_list["deleteline"]) if(alert(usr, "Are you sure you want to delete Line [href_list["deleteline"]]?", "Delete Line" , "Yes" , "No") != "Yes") return var/num = round(text2num(href_list["deleteline"])) if(num > lines.len || num < 1) return lines.Cut(num, num+1) else if(href_list["modifyline"]) var/num = round(text2num(href_list["modifyline"]),1) var/content = input("Enter your line: ", instrumentObj.name, lines[num]) as text|null if(!content || !in_range(instrumentObj, usr)) return if(length(content) > INSTRUMENT_MAX_LINE_LENGTH) content = html_encode(copytext(content, 1, INSTRUMENT_MAX_LINE_LENGTH)) if(num > lines.len || num < 1) return lines[num] = content else if(href_list["stop"]) playing = 0 else if(href_list["moveline"] && href_list["dir"]) var/index = href_list["moveline"] if(!isnum(index)) index = text2num(index) if(!isnum(index)) message_admins("index [index] isn't a number") return var/dir = href_list["dir"] if(!isnum(dir)) dir = text2num(dir) if(!isnum(dir)) message_admins("dir [dir] isn't a number") return if(index > lines.len) return lines.Swap(index, index+dir) ui_interact(usr) return /datum/song/proc/sanitize_tempo(new_tempo) new_tempo = abs(new_tempo) return max(round(new_tempo, world.tick_lag), world.tick_lag) // subclass for handheld instruments, like violin /datum/song/handheld /datum/song/handheld/shouldStopPlaying() if(instrumentObj) return !isliving(instrumentObj.loc) else return 1 ////////////////////////////////////////////////////////////////////////// /obj/structure/piano name = "space piano" desc = "This is a space piano; just like a regular piano, but always in tune! Even if the musician isn't." icon = 'icons/obj/musician.dmi' icon_state = "piano" anchored = 1 density = 1 var/broken = 0 var/datum/song/song /obj/structure/piano/minimoog name = "space minimoog" icon_state = "minimoog" desc = "This is a minimoog; just like a space piano, but more spacey!" /obj/structure/piano/New() ..() if (isobj(loc))//we probably spawned inside a supply crate anchored = FALSE song = new("piano", src) /obj/structure/piano/random/New() ..() if(prob(50)) name = "space minimoog" desc = "This is a minimoog; just like a space piano, but more spacey!" icon_state = "minimoog" /obj/structure/piano/Destroy() QDEL_NULL(song) ..() /obj/structure/piano/initialize() song.tempo = song.sanitize_tempo(song.tempo) // tick_lag isn't set when the map is loaded ..() /obj/structure/piano/attack_hand(mob/user) if(!user.dexterity_check()) to_chat(user, "You don't have the dexterity to do this!") return 1 if(broken) to_chat(user, "\The [src] is broken for good.") return 1 ui_interact(user) /obj/structure/piano/attack_paw(mob/user) return src.attack_hand(user) /obj/structure/piano/ui_interact(mob/user, ui_key="main", datum/nanoui/ui=null, var/force_open=NANOUI_FOCUS) if(!user || !anchored) return user.set_machine(src) song.ui_interact(user,ui_key,ui,force_open) /obj/structure/piano/attackby(obj/item/O, mob/user, params) if (O.is_wrench(user)) if (!anchored && !istype(get_turf(src),/turf/space)) O.playtoolsound(src, 50) user << " You begin to tighten \the [src] to the floor..." if (do_after(user, 20, target = src)) user.visible_message( \ "[user] tightens \the [src]'s casters.", \ "You tighten \the [src]'s casters. Now it can be played again.", \ "You hear a ratchet.") anchored = 1 else if(anchored) O.playtoolsound(src, 50) user << " You begin to loosen \the [src]'s casters..." if (do_after(user, 40, target = src)) user.visible_message( \ "[user] loosens \the [src]'s casters.", \ "You loosen \the [src]. Now it can be pulled somewhere else.", \ "You hear a ratchet.") anchored = 0 else ..() /obj/structure/piano/ex_act(severity) switch(severity) if(1.0) qdel(src) if(2.0) if(broken) qdel(src) else broken = 1 icon_state += "-broken" if(3.0) if(!broken && prob(33)) broken = 1 icon_state += "-broken" /obj/structure/piano/bullet_act(var/obj/item/projectile/Proj) if(Proj.destroy) src.ex_act(2) else if(!istype(Proj ,/obj/item/projectile/beam/lasertag) && !istype(Proj ,/obj/item/projectile/beam/practice) ) if(prob(Proj.damage)) src.ex_act(2) return ..() /obj/structure/piano/xylophone name = "xylophone" desc = "Is this even a real instrument?" icon_state = "xylophone" /obj/structure/piano/xylophone/New() ..() song = new("xylophone", src) song.instrumentExt = "mid"