Merge branch 'master' of https://github.com/Citadel-Station-13/Citadel-Station-13
@@ -14,6 +14,7 @@
|
||||
#define CLOSED_TURF_LAYER 2.05
|
||||
#define ABOVE_NORMAL_TURF_LAYER 2.08
|
||||
#define LATTICE_LAYER 2.2
|
||||
#define OVER_LATTICE_LAYER 2.25
|
||||
#define DISPOSAL_PIPE_LAYER 2.3
|
||||
#define GAS_PIPE_HIDDEN_LAYER 2.35
|
||||
#define WIRE_LAYER 2.4
|
||||
|
||||
@@ -45,4 +45,9 @@
|
||||
#define HYPERSPACE_LAUNCH 2
|
||||
#define HYPERSPACE_END 3
|
||||
|
||||
#define CALL_SHUTTLE_REASON_LENGTH 12
|
||||
#define CALL_SHUTTLE_REASON_LENGTH 12
|
||||
|
||||
//Engine related
|
||||
#define ENGINE_COEFF_MIN 0.5
|
||||
#define ENGINE_COEFF_MAX 2
|
||||
#define ENGINE_DEFAULT_MAXSPEED_ENGINES 5
|
||||
@@ -47,7 +47,7 @@ SUBSYSTEM_DEF(shuttle)
|
||||
|
||||
var/lockdown = FALSE //disallow transit after nuke goes off
|
||||
|
||||
var/auto_call = 99000 //time before in deciseconds in which the shuttle is auto called. Default is 2½ hours plus 15 for the shuttle. So total is 3.
|
||||
var/auto_call = 99000 //time before in deciseconds in which the shuttle is auto called. Default is 2½ hours plus 15 for the shuttle. So total is 3.
|
||||
|
||||
/datum/controller/subsystem/shuttle/Initialize(timeofday)
|
||||
if(!arrivals)
|
||||
@@ -544,3 +544,8 @@ SUBSYSTEM_DEF(shuttle)
|
||||
for(var/obj/docking_port/mobile/M in mobile)
|
||||
if(M.is_in_shuttle_bounds(A))
|
||||
return TRUE
|
||||
|
||||
/datum/controller/subsystem/shuttle/proc/get_containing_shuttle(atom/A)
|
||||
for(var/obj/docking_port/mobile/M in mobile)
|
||||
if(M.is_in_shuttle_bounds(A))
|
||||
return M
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
diff a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm (rejected hunks)
|
||||
@@ -536,3 +536,7 @@ SUBSYSTEM_DEF(shuttle)
|
||||
if(M.is_in_shuttle_bounds(A))
|
||||
return TRUE
|
||||
|
||||
+/datum/controller/subsystem/shuttle/proc/get_containing_shuttle(atom/A)
|
||||
+ for(var/obj/docking_port/mobile/M in mobile)
|
||||
+ if(M.is_in_shuttle_bounds(A))
|
||||
+ return M
|
||||
@@ -29,7 +29,7 @@
|
||||
qdel(H.gloves)
|
||||
|
||||
var/obj/item/clothing/suit/space/space_ninja/theSuit = new(H)
|
||||
var/obj/item/weapon/katana/energy/EK = new(H)
|
||||
var/obj/item/weapon/dash/energy_katana/EK = new(H)
|
||||
theSuit.energyKatana = EK
|
||||
|
||||
H.equip_to_slot_or_del(new /obj/item/device/radio/headset(H), slot_ears)
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
mappath = "[prefix][shuttle_id].dmm"
|
||||
. = ..()
|
||||
|
||||
//Whatever special stuff you want
|
||||
/datum/map_template/shuttle/proc/on_bought()
|
||||
return
|
||||
|
||||
/datum/map_template/shuttle/emergency
|
||||
port_id = "emergency"
|
||||
name = "Base Shuttle Template (Emergency)"
|
||||
@@ -49,6 +53,12 @@
|
||||
// first 10 minutes only
|
||||
return world.time - SSticker.round_start_time < 6000
|
||||
|
||||
/datum/map_template/shuttle/emergency/airless/on_bought()
|
||||
//enable buying engines from cargo
|
||||
var/datum/supply_pack/P = SSshuttle.supply_packs[/datum/supply_pack/engineering/shuttle_engine]
|
||||
P.special_enabled = TRUE
|
||||
|
||||
|
||||
/datum/map_template/shuttle/emergency/asteroid
|
||||
suffix = "asteroid"
|
||||
name = "Asteroid Station Emergency Shuttle"
|
||||
|
||||
@@ -262,7 +262,7 @@
|
||||
|
||||
/obj/item/weapon/nullrod/clown
|
||||
icon = 'icons/obj/wizard.dmi'
|
||||
icon_state = "honkrender"
|
||||
icon_state = "clownrender"
|
||||
item_state = "render"
|
||||
name = "clown dagger"
|
||||
desc = "Used for absolutely hilarious sacrifices."
|
||||
|
||||
@@ -62,6 +62,15 @@
|
||||
new /obj/item/device/multitool(src)
|
||||
new /obj/item/stack/cable_coil(src,30,pick("red","yellow","orange"))
|
||||
|
||||
/obj/item/weapon/storage/belt/utility/full/engi/PopulateContents()
|
||||
new /obj/item/weapon/screwdriver(src)
|
||||
new /obj/item/weapon/wrench(src)
|
||||
new /obj/item/weapon/weldingtool/largetank(src)
|
||||
new /obj/item/weapon/crowbar(src)
|
||||
new /obj/item/weapon/wirecutters(src)
|
||||
new /obj/item/device/multitool(src)
|
||||
new /obj/item/stack/cable_coil(src,30,pick("red","yellow","orange"))
|
||||
|
||||
|
||||
/obj/item/weapon/storage/belt/utility/atmostech/PopulateContents()
|
||||
new /obj/item/weapon/screwdriver(src)
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
for(var/i in 1 to 3)
|
||||
new /obj/item/clothing/head/welding(src)
|
||||
for(var/i in 1 to 3)
|
||||
new /obj/item/weapon/weldingtool/largetank(src)
|
||||
new /obj/item/weapon/weldingtool(src)
|
||||
|
||||
/obj/structure/closet/secure_closet/engineering_personal
|
||||
name = "engineer's locker"
|
||||
|
||||
@@ -91,6 +91,11 @@
|
||||
icon_state = "[icon_state][rand(1, 6)]"
|
||||
..()
|
||||
|
||||
/obj/structure/flora/tree/jungle/small
|
||||
pixel_y = 0
|
||||
pixel_x = -32
|
||||
icon = 'icons/obj/flora/jungletreesmall.dmi'
|
||||
|
||||
//grass
|
||||
/obj/structure/flora/grass
|
||||
name = "grass"
|
||||
@@ -381,5 +386,5 @@
|
||||
pixel_y = -16
|
||||
|
||||
/obj/structure/flora/rock/pile/largejungle/Initialize()
|
||||
..()
|
||||
icon_state = "[initial(icon_state)][rand(1,3)]"
|
||||
. = ..()
|
||||
icon_state = "[initial(icon_state)][rand(1,3)]"
|
||||
|
||||
@@ -1,21 +1,95 @@
|
||||
#define ENGINE_UNWRENCHED 0
|
||||
#define ENGINE_WRENCHED 1
|
||||
#define ENGINE_WELDED 2
|
||||
#define ENGINE_WELDTIME 200
|
||||
|
||||
/obj/structure/shuttle
|
||||
name = "shuttle"
|
||||
icon = 'icons/turf/shuttle.dmi'
|
||||
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
||||
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
||||
obj_integrity = 500
|
||||
max_integrity = 500
|
||||
armor = list(melee = 100, bullet = 10, laser = 10, energy = 0, bomb = 0, bio = 0, rad = 0, fire = 50, acid = 70) //default + ignores melee
|
||||
|
||||
/obj/structure/shuttle/engine
|
||||
name = "engine"
|
||||
density = 1
|
||||
anchored = 1
|
||||
density = TRUE
|
||||
anchored = TRUE
|
||||
var/engine_power = 1
|
||||
var/state = ENGINE_WELDED //welding shmelding
|
||||
|
||||
//Ugh this is a lot of copypasta from emitters, welding need some boilerplate reduction
|
||||
/obj/structure/shuttle/engine/can_be_unfasten_wrench(mob/user, silent)
|
||||
if(state == ENGINE_WELDED)
|
||||
if(!silent)
|
||||
to_chat(user, "<span class='warning'>[src] is welded to the floor!</span>")
|
||||
return FAILED_UNFASTEN
|
||||
return ..()
|
||||
|
||||
/obj/structure/shuttle/engine/default_unfasten_wrench(mob/user, obj/item/weapon/wrench/W, time = 20)
|
||||
. = ..()
|
||||
if(. == SUCCESSFUL_UNFASTEN)
|
||||
if(anchored)
|
||||
state = ENGINE_WRENCHED
|
||||
else
|
||||
state = ENGINE_UNWRENCHED
|
||||
|
||||
/obj/structure/shuttle/engine/attackby(obj/item/I, mob/user, params)
|
||||
add_fingerprint(user)
|
||||
if(default_unfasten_wrench(user, I))
|
||||
return
|
||||
else if(istype(I, /obj/item/weapon/weldingtool))
|
||||
var/obj/item/weapon/weldingtool/WT = I
|
||||
switch(state)
|
||||
if(ENGINE_UNWRENCHED)
|
||||
to_chat(user, "<span class='warning'>The [src.name] needs to be wrenched to the floor!</span>")
|
||||
if(EM_SECURED)
|
||||
if(WT.remove_fuel(0,user))
|
||||
playsound(loc, WT.usesound, 50, 1)
|
||||
user.visible_message("[user.name] starts to weld the [name] to the floor.", \
|
||||
"<span class='notice'>You start to weld \the [src] to the floor...</span>", \
|
||||
"<span class='italics'>You hear welding.</span>")
|
||||
if(do_after(user,ENGINE_WELDTIME*WT.toolspeed, target = src) && WT.isOn())
|
||||
state = ENGINE_WELDED
|
||||
to_chat(user, "<span class='notice'>You weld \the [src] to the floor.</span>")
|
||||
alter_engine_power(engine_power)
|
||||
if(EM_WELDED)
|
||||
if(WT.remove_fuel(0,user))
|
||||
playsound(loc, WT.usesound, 50, 1)
|
||||
user.visible_message("[user.name] starts to cut the [name] free from the floor.", \
|
||||
"<span class='notice'>You start to cut \the [src] free from the floor...</span>", \
|
||||
"<span class='italics'>You hear welding.</span>")
|
||||
if(do_after(user,ENGINE_WELDTIME*WT.toolspeed, target = src) && WT.isOn())
|
||||
state = ENGINE_WRENCHED
|
||||
to_chat(user, "<span class='notice'>You cut \the [src] free from the floor.</span>")
|
||||
alter_engine_power(-engine_power)
|
||||
return
|
||||
else
|
||||
return ..()
|
||||
|
||||
/obj/structure/shuttle/engine/Destroy()
|
||||
if(state == ENGINE_WELDED)
|
||||
alter_engine_power(-engine_power)
|
||||
. = ..()
|
||||
|
||||
//Propagates the change to the shuttle.
|
||||
/obj/structure/shuttle/engine/proc/alter_engine_power(mod)
|
||||
if(mod == 0)
|
||||
return
|
||||
if(SSshuttle.is_in_shuttle_bounds(src))
|
||||
var/obj/docking_port/mobile/M = SSshuttle.get_containing_shuttle(src)
|
||||
if(M)
|
||||
M.alter_engines(mod)
|
||||
|
||||
/obj/structure/shuttle/engine/heater
|
||||
name = "heater"
|
||||
icon_state = "heater"
|
||||
engine_power = 0 // todo make these into 2x1 parts
|
||||
|
||||
/obj/structure/shuttle/engine/platform
|
||||
name = "platform"
|
||||
icon_state = "platform"
|
||||
engine_power = 0
|
||||
|
||||
/obj/structure/shuttle/engine/propulsion
|
||||
name = "propulsion"
|
||||
@@ -25,6 +99,10 @@
|
||||
/obj/structure/shuttle/engine/propulsion/burst
|
||||
name = "burst"
|
||||
|
||||
/obj/structure/shuttle/engine/propulsion/burst/cargo
|
||||
state = ENGINE_UNWRENCHED
|
||||
anchored = FALSE
|
||||
|
||||
/obj/structure/shuttle/engine/propulsion/burst/left
|
||||
name = "left"
|
||||
icon_state = "burst_l"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
diff a/code/game/shuttle_engines.dm b/code/game/shuttle_engines.dm (rejected hunks)
|
||||
@@ -46,7 +117,7 @@
|
||||
bound_height = 64
|
||||
appearance_flags = 0
|
||||
|
||||
-obj/structure/shuttle/engine/huge
|
||||
+/obj/structure/shuttle/engine/huge
|
||||
name = "engine"
|
||||
opacity = 1
|
||||
icon = 'icons/obj/3x3.dmi'
|
||||
@@ -116,6 +116,23 @@
|
||||
underlay_appearance.icon_state = "necro1"
|
||||
return TRUE
|
||||
|
||||
/turf/closed/indestructible/riveted/boss
|
||||
name = "stone wall"
|
||||
desc = "A thick, seemingly indestructible stone wall."
|
||||
icon = 'icons/turf/walls/boss_wall.dmi'
|
||||
icon_state = "wall"
|
||||
explosion_block = 50
|
||||
baseturf = /turf/closed/indestructible/riveted/boss
|
||||
|
||||
/turf/closed/indestructible/riveted/boss/hot
|
||||
icon = 'icons/turf/walls/boss_wall_hot.dmi'
|
||||
baseturf = /turf/closed/indestructible/riveted/boss/hot
|
||||
|
||||
/turf/closed/indestructible/riveted/boss/get_smooth_underlay_icon(mutable_appearance/underlay_appearance, turf/asking_turf, adjacency_dir)
|
||||
underlay_appearance.icon = 'icons/turf/floors.dmi'
|
||||
underlay_appearance.icon_state = "basalt"
|
||||
return TRUE
|
||||
|
||||
/turf/closed/indestructible/riveted/hierophant
|
||||
name = "wall"
|
||||
desc = "A wall made out of a strange metal. The squares on it pulse in a predictable pattern."
|
||||
|
||||
@@ -54,8 +54,16 @@
|
||||
else
|
||||
to_chat(user, "<span class='warning'>The plating is going to need some support! Place metal rods first.</span>")
|
||||
|
||||
/turf/open/chasm/proc/is_safe()
|
||||
//if anything matching this typecache is found in the chasm, we don't drop things
|
||||
var/static/list/chasm_safeties_typecache = typecacheof(list(/obj/structure/lattice/catwalk, /obj/structure/stone_tile))
|
||||
var/list/found_safeties = typecache_filter_list(contents, chasm_safeties_typecache)
|
||||
return LAZYLEN(found_safeties)
|
||||
|
||||
/turf/open/chasm/proc/drop_stuff(AM)
|
||||
. = 0
|
||||
if(is_safe())
|
||||
return FALSE
|
||||
var/thing_to_check = src
|
||||
if(AM)
|
||||
thing_to_check = list(AM)
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
/turf/open/floor/plating/lava/proc/is_safe()
|
||||
//if anything matching this typecache is found in the lava, we don't burn things
|
||||
var/static/list/lava_safeties_typecache = typecacheof(list(/obj/structure/lattice/catwalk))
|
||||
var/static/list/lava_safeties_typecache = typecacheof(list(/obj/structure/lattice/catwalk, /obj/structure/stone_tile))
|
||||
var/list/found_safeties = typecache_filter_list(contents, lava_safeties_typecache)
|
||||
return LAZYLEN(found_safeties)
|
||||
|
||||
|
||||
@@ -710,6 +710,15 @@
|
||||
/obj/item/weapon/am_containment)
|
||||
crate_name = "antimatter jar crate"
|
||||
|
||||
/datum/supply_pack/engineering/shuttle_engine
|
||||
name = "Shuttle Engine Crate"
|
||||
cost = 5000
|
||||
access = GLOB.access_ce
|
||||
contains = list(/obj/structure/shuttle/engine/propulsion/burst/cargo)
|
||||
crate_name = "shuttle engine crate"
|
||||
crate_type = /obj/structure/closet/crate/secure/engineering
|
||||
special = TRUE
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////// Medical /////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -83,7 +83,7 @@ Station Engineer
|
||||
name = "Station Engineer"
|
||||
jobtype = /datum/job/engineer
|
||||
|
||||
belt = /obj/item/weapon/storage/belt/utility/full
|
||||
belt = /obj/item/weapon/storage/belt/utility/full/engi
|
||||
l_pocket = /obj/item/device/pda/engineering
|
||||
ears = /obj/item/device/radio/headset/headset_eng
|
||||
uniform = /obj/item/clothing/under/rank/engineer
|
||||
|
||||
@@ -309,7 +309,7 @@
|
||||
/obj/item/weapon/melee/supermatter_sword,
|
||||
/obj/item/weapon/shield/changeling,
|
||||
/obj/item/weapon/lava_staff,
|
||||
/obj/item/weapon/katana/energy,
|
||||
/obj/item/weapon/dash/energy_katana,
|
||||
/obj/item/weapon/hierophant_club,
|
||||
/obj/item/weapon/his_grace,
|
||||
/obj/item/weapon/gun/ballistic/minigun,
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
var/blacklisted_turfs
|
||||
var/whitelisted_turfs
|
||||
var/banned_areas
|
||||
var/banned_objects
|
||||
|
||||
/datum/map_template/shelter/New()
|
||||
. = ..()
|
||||
blacklisted_turfs = typecacheof(/turf/closed)
|
||||
whitelisted_turfs = list()
|
||||
banned_areas = typecacheof(/area/shuttle)
|
||||
banned_objects = list()
|
||||
|
||||
/datum/map_template/shelter/proc/check_deploy(turf/deploy_location)
|
||||
var/affected = get_affected_turfs(deploy_location, centered=TRUE)
|
||||
@@ -24,7 +26,7 @@
|
||||
return SHELTER_DEPLOY_BAD_TURFS
|
||||
|
||||
for(var/obj/O in T)
|
||||
if(O.density && O.anchored)
|
||||
if((O.density && O.anchored) || is_type_in_typecache(O, banned_objects))
|
||||
return SHELTER_DEPLOY_ANCHORED_OBJECTS
|
||||
return SHELTER_DEPLOY_ALLOWED
|
||||
|
||||
@@ -40,7 +42,7 @@
|
||||
/datum/map_template/shelter/alpha/New()
|
||||
. = ..()
|
||||
whitelisted_turfs = typecacheof(/turf/closed/mineral)
|
||||
|
||||
banned_objects = typecacheof(/obj/structure/stone_tile)
|
||||
|
||||
/datum/map_template/shelter/beta
|
||||
name = "Shelter Beta"
|
||||
@@ -55,3 +57,4 @@
|
||||
/datum/map_template/shelter/beta/New()
|
||||
. = ..()
|
||||
whitelisted_turfs = typecacheof(/turf/closed/mineral)
|
||||
banned_objects = typecacheof(/obj/structure/stone_tile)
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
|
||||
. = ..()
|
||||
|
||||
/mob/living/silicon/ai/IgniteMob()
|
||||
fire_stacks = 0
|
||||
. = ..()
|
||||
|
||||
/mob/living/silicon/ai/verb/pick_icon()
|
||||
set category = "AI Commands"
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
stat = DEAD
|
||||
canmove = 0
|
||||
card.removePersonality()
|
||||
if(holoform)
|
||||
card.forceMove(loc)
|
||||
card.forceMove(loc)
|
||||
update_sight()
|
||||
clear_fullscreens()
|
||||
|
||||
//New pAI's get a brand new mind to prevent meta stuff from their previous life. This new mind causes problems down the line if it's not deleted here.
|
||||
GLOB.living_mob_list -= src
|
||||
ghostize()
|
||||
qdel(src)
|
||||
qdel(src)
|
||||
|
||||
@@ -316,7 +316,7 @@ Difficulty: Medium
|
||||
for(var/mob/living/L in T.contents)
|
||||
if(istype(L, /mob/living/simple_animal/hostile/megafauna/dragon))
|
||||
continue
|
||||
if(!islist(flame_hit) || !flame_hit[L])
|
||||
if(islist(flame_hit) && !flame_hit[L])
|
||||
L.adjustFireLoss(40)
|
||||
to_chat(L, "<span class='userdanger'>You're hit by the drake's fire breath!</span>")
|
||||
flame_hit[L] = TRUE
|
||||
|
||||
@@ -350,7 +350,7 @@ Difficulty: Hard
|
||||
INVOKE_ASYNC(src, .proc/arena_squares, T, d)
|
||||
for(var/t in RANGE_TURFS(11, T))
|
||||
if(t && get_dist(t, T) == 11)
|
||||
new /obj/effect/temp_visual/hierophant/wall(t)
|
||||
new /obj/effect/temp_visual/hierophant/wall(t, src)
|
||||
new /obj/effect/temp_visual/hierophant/blast(t, src, FALSE)
|
||||
if(get_dist(src, T) >= 11) //hey you're out of range I need to get closer to you!
|
||||
INVOKE_ASYNC(src, .proc/blink, T)
|
||||
@@ -469,6 +469,8 @@ Difficulty: Hard
|
||||
return ..()
|
||||
|
||||
/obj/effect/temp_visual/hierophant/wall/CanPass(atom/movable/mover, turf/target, height = 0)
|
||||
if(QDELETED(caster))
|
||||
return FALSE
|
||||
if(mover == caster.pulledby)
|
||||
return TRUE
|
||||
if(istype(mover, /obj/item/projectile))
|
||||
|
||||
@@ -1,30 +1,93 @@
|
||||
/obj/item/weapon/katana/energy
|
||||
/obj/item/weapon/dash
|
||||
name = "abstract dash weapon"
|
||||
var/max_charges = 3
|
||||
var/current_charges = 3
|
||||
var/charge_rate = 30 //In deciseconds
|
||||
var/dash_toggled = TRUE
|
||||
|
||||
var/bypass_density = FALSE //Can we beam past windows/airlocks/etc
|
||||
|
||||
var/start_effect_type = /obj/effect/temp_visual/dir_setting/ninja/phase/out
|
||||
var/end_effect_type = /obj/effect/temp_visual/dir_setting/ninja/phase
|
||||
var/beam_icon_state = "blur"
|
||||
var/dash_beam_type = /obj/effect/ebeam
|
||||
|
||||
/obj/item/weapon/dash/proc/charge()
|
||||
current_charges = Clamp(current_charges + 1, 0, max_charges)
|
||||
if(istype(loc, /mob/living))
|
||||
to_chat(loc, "<span class='notice'>[src] now has [current_charges]/[max_charges] charges.</span>")
|
||||
|
||||
/obj/item/weapon/dash/attack_self(mob/user)
|
||||
dash_toggled = !dash_toggled
|
||||
to_chat(user, "<span class='notice'>You [dash_toggled ? "enable" : "disable"] the dash function on [src].</span>")
|
||||
|
||||
/obj/item/weapon/dash/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
|
||||
if(dash_toggled)
|
||||
dash(user, target)
|
||||
return
|
||||
|
||||
/obj/item/weapon/dash/proc/dash(mob/user, atom/target)
|
||||
if(!current_charges)
|
||||
return
|
||||
|
||||
if(Adjacent(target))
|
||||
return
|
||||
|
||||
if(target.density)
|
||||
return
|
||||
|
||||
var/turf/T = get_turf(target)
|
||||
|
||||
if(!bypass_density)
|
||||
for(var/turf/turf in getline(get_turf(user),T))
|
||||
for(var/atom/A in turf)
|
||||
if(A.density)
|
||||
return
|
||||
|
||||
if(target in view(user.client.view, get_turf(user)))
|
||||
var/obj/spot1 = new start_effect_type(T, user.dir)
|
||||
user.forceMove(T)
|
||||
playsound(T, 'sound/magic/blink.ogg', 25, 1)
|
||||
playsound(T, "sparks", 50, 1)
|
||||
var/obj/spot2 = new end_effect_type(get_turf(user), user.dir)
|
||||
spot1.Beam(spot2, beam_icon_state,time = 2, maxdistance = 20, beam_type = dash_beam_type)
|
||||
current_charges--
|
||||
addtimer(CALLBACK(src, .proc/charge), charge_rate)
|
||||
|
||||
/obj/item/weapon/dash/energy_katana
|
||||
name = "energy katana"
|
||||
desc = "A katana infused with strong energy."
|
||||
icon_state = "energy_katana"
|
||||
item_state = "energy_katana"
|
||||
force = 40
|
||||
throwforce = 20
|
||||
block_chance = 50
|
||||
armour_penetration = 50
|
||||
w_class = WEIGHT_CLASS_NORMAL
|
||||
hitsound = 'sound/weapons/bladeslice.ogg'
|
||||
attack_verb = list("attacked", "slashed", "stabbed", "sliced", "torn", "ripped", "diced", "cut")
|
||||
block_chance = 50
|
||||
sharpness = IS_SHARP
|
||||
obj_integrity = 200
|
||||
max_integrity = 200
|
||||
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
||||
bypass_density = TRUE
|
||||
var/datum/effect_system/spark_spread/spark_system
|
||||
|
||||
/obj/item/weapon/katana/energy/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
|
||||
if(!user || !target)
|
||||
return
|
||||
|
||||
if(proximity_flag)
|
||||
if(isobj(target) || issilicon(target))
|
||||
spark_system.start()
|
||||
playsound(user, "sparks", 50, 1)
|
||||
playsound(user, 'sound/weapons/blade1.ogg', 50, 1)
|
||||
target.emag_act(user)
|
||||
/obj/item/weapon/dash/energy_katana/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
|
||||
if(dash_toggled)
|
||||
return ..()
|
||||
if(proximity_flag && (isobj(target) || issilicon(target)))
|
||||
spark_system.start()
|
||||
playsound(user, "sparks", 50, 1)
|
||||
playsound(user, 'sound/weapons/blade1.ogg', 50, 1)
|
||||
target.emag_act(user)
|
||||
|
||||
|
||||
//If we hit the Ninja who owns this Katana, they catch it.
|
||||
//Works for if the Ninja throws it or it throws itself or someone tries
|
||||
//To throw it at the ninja
|
||||
/obj/item/weapon/katana/energy/throw_impact(atom/hit_atom)
|
||||
/obj/item/weapon/dash/energy_katana/throw_impact(atom/hit_atom)
|
||||
if(ishuman(hit_atom))
|
||||
var/mob/living/carbon/human/H = hit_atom
|
||||
if(istype(H.wear_suit, /obj/item/clothing/suit/space/space_ninja))
|
||||
@@ -35,7 +98,7 @@
|
||||
|
||||
..()
|
||||
|
||||
/obj/item/weapon/katana/energy/proc/returnToOwner(mob/living/carbon/human/user, doSpark = 1, caught = 0)
|
||||
/obj/item/weapon/dash/energy_katana/proc/returnToOwner(mob/living/carbon/human/user, doSpark = 1, caught = 0)
|
||||
if(!istype(user))
|
||||
return
|
||||
forceMove(get_turf(user))
|
||||
@@ -62,13 +125,12 @@
|
||||
if(msg)
|
||||
to_chat(user, "<span class='notice'>[msg]</span>")
|
||||
|
||||
/obj/item/weapon/katana/energy/New()
|
||||
..()
|
||||
/obj/item/weapon/dash/energy_katana/Initialize()
|
||||
. = ..()
|
||||
spark_system = new /datum/effect_system/spark_spread()
|
||||
spark_system.set_up(5, 0, src)
|
||||
spark_system.attach(src)
|
||||
|
||||
/obj/item/weapon/katana/energy/Destroy()
|
||||
qdel(spark_system)
|
||||
spark_system = null
|
||||
return ..()
|
||||
/obj/item/weapon/dash/energy_katana/Destroy()
|
||||
QDEL_NULL(spark_system)
|
||||
return ..()
|
||||
|
||||
@@ -22,7 +22,7 @@ Contents:
|
||||
armor = list(melee = 60, bullet = 50, laser = 30,energy = 15, bomb = 30, bio = 30, rad = 30, fire = 100, acid = 100)
|
||||
strip_delay = 12
|
||||
|
||||
actions_types = list(/datum/action/item_action/initialize_ninja_suit, /datum/action/item_action/ninjajaunt, /datum/action/item_action/ninjasmoke, /datum/action/item_action/ninjaboost, /datum/action/item_action/ninjapulse, /datum/action/item_action/ninjastar, /datum/action/item_action/ninjanet, /datum/action/item_action/ninja_sword_recall, /datum/action/item_action/ninja_stealth, /datum/action/item_action/toggle_glove)
|
||||
actions_types = list(/datum/action/item_action/initialize_ninja_suit, /datum/action/item_action/ninjasmoke, /datum/action/item_action/ninjaboost, /datum/action/item_action/ninjapulse, /datum/action/item_action/ninjastar, /datum/action/item_action/ninjanet, /datum/action/item_action/ninja_sword_recall, /datum/action/item_action/ninja_stealth, /datum/action/item_action/toggle_glove)
|
||||
|
||||
//Important parts of the suit.
|
||||
var/mob/living/carbon/human/affecting = null
|
||||
@@ -31,7 +31,7 @@ Contents:
|
||||
var/list/reagent_list = list("omnizine","salbutamol","spaceacillin","charcoal","nutriment","radium","potass_iodide")//The reagents ids which are added to the suit at New().
|
||||
var/list/stored_research = list()//For stealing station research.
|
||||
var/obj/item/weapon/disk/tech_disk/t_disk//To copy design onto disk.
|
||||
var/obj/item/weapon/katana/energy/energyKatana //For teleporting the katana back to the ninja (It's an ability)
|
||||
var/obj/item/weapon/dash/energy_katana/energyKatana //For teleporting the katana back to the ninja (It's an ability)
|
||||
|
||||
//Other articles of ninja gear worn together, used to easily reference them after initializing.
|
||||
var/obj/item/clothing/head/helmet/space/space_ninja/n_hood
|
||||
@@ -181,9 +181,6 @@ Contents:
|
||||
if(!s_initialized)
|
||||
to_chat(user, "<span class='warning'><b>ERROR</b>: suit offline. Please activate suit.</span>")
|
||||
return FALSE
|
||||
if(istype(action, /datum/action/item_action/ninjajaunt))
|
||||
ninjajaunt()
|
||||
return TRUE
|
||||
if(istype(action, /datum/action/item_action/ninjasmoke))
|
||||
ninjasmoke()
|
||||
return TRUE
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
|
||||
/obj/item/clothing/suit/space/space_ninja/proc/deinitialize_three(delay, mob/living/carbon/human/U)
|
||||
to_chat(U, "<span class='notice'>Logging off, [U:real_name]. Shutting down <B>SpiderOS</B>.</span>")
|
||||
remove_ninja_verbs()
|
||||
addtimer(CALLBACK(src, .proc/deinitialize_four, delay, U), delay)
|
||||
|
||||
/obj/item/clothing/suit/space/space_ninja/proc/deinitialize_four(delay, mob/living/carbon/human/U)
|
||||
|
||||
@@ -22,11 +22,5 @@ Contents:
|
||||
|
||||
|
||||
/obj/item/clothing/suit/space/space_ninja/proc/grant_ninja_verbs()
|
||||
verbs += /obj/item/clothing/suit/space/space_ninja/proc/ninjashift
|
||||
|
||||
s_initialized=1
|
||||
slowdown=0
|
||||
|
||||
|
||||
/obj/item/clothing/suit/space/space_ninja/proc/remove_ninja_verbs()
|
||||
verbs -= /obj/item/clothing/suit/space/space_ninja/proc/ninjashift
|
||||
slowdown=0
|
||||
@@ -87,6 +87,8 @@
|
||||
new impact_effect_type(target_loca, target, src)
|
||||
return 0
|
||||
var/mob/living/L = target
|
||||
if(L.buckled && ismob(L.buckled))
|
||||
L = L.buckled
|
||||
if(blocked != 100) // not completely blocked
|
||||
if(damage && L.blood_volume && damage_type == BRUTE)
|
||||
var/splatter_dir = dir
|
||||
|
||||
@@ -60,3 +60,168 @@
|
||||
. = ..()
|
||||
else
|
||||
return QDEL_HINT_LETMELIVE
|
||||
|
||||
//stone tiles for boss arenas
|
||||
/obj/structure/stone_tile
|
||||
name = "stone tile"
|
||||
icon = 'icons/turf/boss_floors.dmi'
|
||||
icon_state = "pristine_tile1"
|
||||
layer = OVER_LATTICE_LAYER
|
||||
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
|
||||
var/tile_key = "pristine_tile"
|
||||
var/tile_random_sprite_max = 24
|
||||
|
||||
/obj/structure/stone_tile/Initialize(mapload)
|
||||
. = ..()
|
||||
icon_state = "[tile_key][rand(1, tile_random_sprite_max)]"
|
||||
|
||||
/obj/structure/stone_tile/block
|
||||
name = "stone block"
|
||||
icon_state = "pristine_block1"
|
||||
tile_key = "pristine_block"
|
||||
tile_random_sprite_max = 4
|
||||
|
||||
/obj/structure/stone_tile/slab
|
||||
name = "stone slab"
|
||||
icon_state = "pristine_slab1"
|
||||
tile_key = "pristine_slab"
|
||||
tile_random_sprite_max = 4
|
||||
|
||||
/obj/structure/stone_tile/center
|
||||
name = "stone center tile"
|
||||
icon_state = "pristine_center1"
|
||||
tile_key = "pristine_center"
|
||||
tile_random_sprite_max = 4
|
||||
|
||||
/obj/structure/stone_tile/surrounding
|
||||
name = "stone surrounding slab"
|
||||
icon_state = "pristine_surrounding1"
|
||||
tile_key = "pristine_surrounding"
|
||||
tile_random_sprite_max = 2
|
||||
|
||||
/obj/structure/stone_tile/surrounding_tile
|
||||
name = "stone surrounding tile"
|
||||
icon_state = "pristine_surrounding_tile1"
|
||||
tile_key = "pristine_surrounding_tile"
|
||||
tile_random_sprite_max = 2
|
||||
|
||||
//cracked stone tiles
|
||||
/obj/structure/stone_tile/cracked
|
||||
name = "cracked stone tile"
|
||||
icon_state = "cracked_tile1"
|
||||
tile_key = "cracked_tile"
|
||||
|
||||
/obj/structure/stone_tile/block/cracked
|
||||
name = "cracked stone block"
|
||||
icon_state = "cracked_block1"
|
||||
tile_key = "cracked_block"
|
||||
|
||||
/obj/structure/stone_tile/slab/cracked
|
||||
name = "cracked stone slab"
|
||||
icon_state = "cracked_slab1"
|
||||
tile_key = "cracked_slab"
|
||||
tile_random_sprite_max = 1
|
||||
|
||||
/obj/structure/stone_tile/center/cracked
|
||||
name = "cracked stone center tile"
|
||||
icon_state = "cracked_center1"
|
||||
tile_key = "cracked_center"
|
||||
|
||||
/obj/structure/stone_tile/surrounding/cracked
|
||||
name = "cracked stone surrounding slab"
|
||||
icon_state = "cracked_surrounding1"
|
||||
tile_key = "cracked_surrounding"
|
||||
tile_random_sprite_max = 1
|
||||
|
||||
/obj/structure/stone_tile/surrounding_tile/cracked
|
||||
name = "cracked stone surrounding tile"
|
||||
icon_state = "cracked_surrounding_tile1"
|
||||
tile_key = "cracked_surrounding_tile"
|
||||
|
||||
//burnt stone tiles
|
||||
/obj/structure/stone_tile/burnt
|
||||
name = "burnt stone tile"
|
||||
icon_state = "burnt_tile1"
|
||||
tile_key = "burnt_tile"
|
||||
|
||||
/obj/structure/stone_tile/block/burnt
|
||||
name = "burnt stone block"
|
||||
icon_state = "burnt_block1"
|
||||
tile_key = "burnt_block"
|
||||
|
||||
/obj/structure/stone_tile/slab/burnt
|
||||
name = "burnt stone slab"
|
||||
icon_state = "burnt_slab1"
|
||||
tile_key = "burnt_slab"
|
||||
|
||||
/obj/structure/stone_tile/center/burnt
|
||||
name = "burnt stone center tile"
|
||||
icon_state = "burnt_center1"
|
||||
tile_key = "burnt_center"
|
||||
|
||||
/obj/structure/stone_tile/surrounding/burnt
|
||||
name = "burnt stone surrounding slab"
|
||||
icon_state = "burnt_surrounding1"
|
||||
tile_key = "burnt_surrounding"
|
||||
|
||||
/obj/structure/stone_tile/surrounding_tile/burnt
|
||||
name = "burnt stone surrounding tile"
|
||||
icon_state = "burnt_surrounding_tile1"
|
||||
tile_key = "burnt_surrounding_tile"
|
||||
|
||||
//hot stone tiles, cosmetic only
|
||||
/obj/structure/stone_tile/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/block/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/slab/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/center/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/surrounding/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/surrounding_tile/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
//hot cracked stone tiles, cosmetic only
|
||||
/obj/structure/stone_tile/cracked/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/block/cracked/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/slab/cracked/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/center/cracked/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/surrounding/cracked/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/surrounding_tile/cracked/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
//hot burnt stone tiles, cosmetic only
|
||||
/obj/structure/stone_tile/burnt/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/block/burnt/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/slab/burnt/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/center/burnt/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/surrounding/burnt/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
/obj/structure/stone_tile/surrounding_tile/burnt/hot
|
||||
icon = 'icons/turf/boss_floors_hot.dmi'
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
set_coefficient = 1
|
||||
else
|
||||
set_coefficient = 0.5
|
||||
var/call_time = SSshuttle.emergencyCallTime * set_coefficient
|
||||
var/call_time = SSshuttle.emergencyCallTime * set_coefficient * engine_coeff
|
||||
switch(mode)
|
||||
// The shuttle can not normally be called while "recalling", so
|
||||
// if this proc is called, it's via admin fiat
|
||||
@@ -359,7 +359,7 @@
|
||||
enterTransit()
|
||||
mode = SHUTTLE_ESCAPE
|
||||
launch_status = ENDGAME_LAUNCHED
|
||||
setTimer(SSshuttle.emergencyEscapeTime)
|
||||
setTimer(SSshuttle.emergencyEscapeTime * engine_coeff)
|
||||
priority_announce("The Emergency Shuttle has left the station. Estimate [timeLeft(600)] minutes until the shuttle docks at Central Command.", null, null, "Priority")
|
||||
|
||||
if(SHUTTLE_STRANDED)
|
||||
|
||||
@@ -285,6 +285,9 @@
|
||||
|
||||
message_admins(msg)
|
||||
WARNING(msg)
|
||||
return
|
||||
//Everything fine
|
||||
S.on_bought()
|
||||
|
||||
/obj/machinery/shuttle_manipulator/proc/unload_preview()
|
||||
if(preview_shuttle)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
|
||||
anchored = 1
|
||||
|
||||
//
|
||||
var/id
|
||||
// this should point -away- from the dockingport door, ie towards the ship
|
||||
dir = NORTH
|
||||
@@ -224,11 +224,9 @@
|
||||
var/timid = FALSE
|
||||
|
||||
var/list/ripples = list()
|
||||
|
||||
/obj/docking_port/mobile/Initialize()
|
||||
. = ..()
|
||||
if(!timid)
|
||||
register()
|
||||
var/engine_coeff = 1 //current engine coeff
|
||||
var/current_engines = 0 //current engine power
|
||||
var/initial_engines = 0 //initial engine power
|
||||
|
||||
/obj/docking_port/mobile/proc/register()
|
||||
SSshuttle.mobile += src
|
||||
@@ -244,6 +242,8 @@
|
||||
|
||||
/obj/docking_port/mobile/Initialize(mapload)
|
||||
. = ..()
|
||||
if(!timid)
|
||||
register()
|
||||
|
||||
var/area/A = get_area(src)
|
||||
if(istype(A, /area/shuttle))
|
||||
@@ -259,6 +259,9 @@
|
||||
areaInstance.name = name
|
||||
areaInstance.contents += return_ordered_turfs()
|
||||
|
||||
initial_engines = count_engines()
|
||||
current_engines = initial_engines
|
||||
|
||||
#ifdef DOCKING_PORT_HIGHLIGHT
|
||||
highlight("#0f0")
|
||||
#endif
|
||||
@@ -325,17 +328,17 @@
|
||||
switch(mode)
|
||||
if(SHUTTLE_CALL)
|
||||
if(S == destination)
|
||||
if(timeLeft(1) < callTime)
|
||||
setTimer(callTime)
|
||||
if(timeLeft(1) < callTime * engine_coeff)
|
||||
setTimer(callTime * engine_coeff)
|
||||
else
|
||||
destination = S
|
||||
setTimer(callTime)
|
||||
setTimer(callTime * engine_coeff)
|
||||
if(SHUTTLE_RECALL)
|
||||
if(S == destination)
|
||||
setTimer(callTime - timeLeft(1))
|
||||
setTimer(callTime * engine_coeff - timeLeft(1))
|
||||
else
|
||||
destination = S
|
||||
setTimer(callTime)
|
||||
setTimer(callTime * engine_coeff)
|
||||
mode = SHUTTLE_CALL
|
||||
if(SHUTTLE_IDLE, SHUTTLE_IGNITING)
|
||||
destination = S
|
||||
@@ -621,7 +624,7 @@
|
||||
return
|
||||
else
|
||||
mode = SHUTTLE_CALL
|
||||
setTimer(callTime)
|
||||
setTimer(callTime * engine_coeff)
|
||||
enterTransit()
|
||||
return
|
||||
|
||||
@@ -685,7 +688,7 @@
|
||||
|
||||
var/ds_remaining
|
||||
if(!timer)
|
||||
ds_remaining = callTime
|
||||
ds_remaining = callTime * engine_coeff
|
||||
else
|
||||
ds_remaining = max(0, timer - world.time)
|
||||
|
||||
@@ -765,5 +768,62 @@
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
// Losing all initial engines should get you 2
|
||||
// Adding another set of engines at 0.5 time
|
||||
/obj/docking_port/mobile/proc/alter_engines(mod)
|
||||
if(mod == 0)
|
||||
return
|
||||
var/old_coeff = engine_coeff
|
||||
engine_coeff = get_engine_coeff(current_engines,mod)
|
||||
current_engines = max(0,current_engines + mod)
|
||||
if(in_flight())
|
||||
var/delta_coeff = engine_coeff / old_coeff
|
||||
modTimer(delta_coeff)
|
||||
|
||||
/obj/docking_port/mobile/proc/count_engines()
|
||||
. = 0
|
||||
for(var/obj/structure/shuttle/engine/E in areaInstance.contents)
|
||||
if(!QDELETED(E))
|
||||
. += E.engine_power
|
||||
|
||||
// Double initial engines to get to 0.5 minimum
|
||||
// Lose all initial engines to get to 2
|
||||
//For 0 engine shuttles like BYOS 5 engines to get to doublespeed
|
||||
/obj/docking_port/mobile/proc/get_engine_coeff(current,engine_mod)
|
||||
var/new_value = max(0,current + engine_mod)
|
||||
if(new_value == initial_engines)
|
||||
return 1
|
||||
if(new_value > initial_engines)
|
||||
var/delta = new_value - initial_engines
|
||||
var/change_per_engine = (1 - ENGINE_COEFF_MIN) / ENGINE_DEFAULT_MAXSPEED_ENGINES // 5 by default
|
||||
if(initial_engines > 0)
|
||||
change_per_engine = (1 - ENGINE_COEFF_MIN) / initial_engines // or however many it had
|
||||
return Clamp(1 - delta * change_per_engine,ENGINE_COEFF_MIN,ENGINE_COEFF_MAX)
|
||||
if(new_value < initial_engines)
|
||||
var/delta = initial_engines - new_value
|
||||
var/change_per_engine = 1 //doesn't really matter should not be happening for 0 engine shuttles
|
||||
if(initial_engines > 0)
|
||||
change_per_engine = (ENGINE_COEFF_MAX - 1) / initial_engines //just linear drop to max delay
|
||||
return Clamp(1 + delta * change_per_engine,ENGINE_COEFF_MIN,ENGINE_COEFF_MAX)
|
||||
|
||||
|
||||
/obj/docking_port/mobile/proc/in_flight()
|
||||
switch(mode)
|
||||
if(SHUTTLE_CALL,SHUTTLE_RECALL)
|
||||
return TRUE
|
||||
if(SHUTTLE_IDLE,SHUTTLE_IGNITING)
|
||||
return FALSE
|
||||
else
|
||||
return FALSE // hmm
|
||||
|
||||
/obj/docking_port/mobile/emergency/in_flight()
|
||||
switch(mode)
|
||||
if(SHUTTLE_ESCAPE)
|
||||
return TRUE
|
||||
if(SHUTTLE_STRANDED,SHUTTLE_ENDGAME)
|
||||
return FALSE
|
||||
else
|
||||
return ..()
|
||||
|
||||
|
||||
#undef DOCKING_PORT_HIGHLIGHT
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
author: "BeeSting12"
|
||||
delete-after: True
|
||||
changes:
|
||||
- bugfix: "The clowndagger now has the correct skin."
|
||||
@@ -0,0 +1,5 @@
|
||||
author: "Steelpoint"
|
||||
delete-after: True
|
||||
changes:
|
||||
- rscadd: "Station Engineers spawn with a Industrial Welder in their toolbelt."
|
||||
- tweak: "Engineering Welder Locker now only holds three standard Welders."
|
||||
@@ -0,0 +1,5 @@
|
||||
author: "Kor"
|
||||
delete-after: True
|
||||
changes:
|
||||
- rscadd: "Added dash weapons, which let you do a short teleport within line of sight."
|
||||
- rscadd: "The ninjas energy katana now lets him dash. He can no longer teleport with right click."
|
||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1875,7 +1875,6 @@
|
||||
#include "code\modules\ninja\suit\n_suit_verbs\ninja_stars.dm"
|
||||
#include "code\modules\ninja\suit\n_suit_verbs\ninja_stealth.dm"
|
||||
#include "code\modules\ninja\suit\n_suit_verbs\ninja_sword_recall.dm"
|
||||
#include "code\modules\ninja\suit\n_suit_verbs\ninja_teleporting.dm"
|
||||
#include "code\modules\orbit\orbit.dm"
|
||||
#include "code\modules\paperwork\clipboard.dm"
|
||||
#include "code\modules\paperwork\contract.dm"
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 11.00
|
||||
# Visual C# Express 2010
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "midi2piano", "midi2piano\midi2piano.csproj", "{68C84B61-F710-491C-BEE8-5E362C167897}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|Mixed Platforms = Debug|Mixed Platforms
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|Mixed Platforms = Release|Mixed Platforms
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Debug|Any CPU.ActiveCfg = Debug|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Debug|Mixed Platforms.Build.0 = Debug|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Debug|x86.Build.0 = Debug|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Release|Any CPU.ActiveCfg = Release|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Release|Mixed Platforms.ActiveCfg = Release|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Release|Mixed Platforms.Build.0 = Release|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Release|x86.ActiveCfg = Release|x86
|
||||
{68C84B61-F710-491C-BEE8-5E362C167897}.Release|x86.Build.0 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,135 +0,0 @@
|
||||
namespace midi2piano
|
||||
{
|
||||
partial class Form1
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.OutputTxt = new System.Windows.Forms.TextBox();
|
||||
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
|
||||
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.importMIDIToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.copyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.importDlg = new System.Windows.Forms.OpenFileDialog();
|
||||
this.halpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.menuStrip1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// OutputTxt
|
||||
//
|
||||
this.OutputTxt.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.OutputTxt.Location = new System.Drawing.Point(0, 24);
|
||||
this.OutputTxt.Multiline = true;
|
||||
this.OutputTxt.Name = "OutputTxt";
|
||||
this.OutputTxt.ReadOnly = true;
|
||||
this.OutputTxt.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.OutputTxt.Size = new System.Drawing.Size(284, 240);
|
||||
this.OutputTxt.TabIndex = 0;
|
||||
//
|
||||
// menuStrip1
|
||||
//
|
||||
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.fileToolStripMenuItem,
|
||||
this.copyToolStripMenuItem,
|
||||
this.halpToolStripMenuItem});
|
||||
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
|
||||
this.menuStrip1.Name = "menuStrip1";
|
||||
this.menuStrip1.Size = new System.Drawing.Size(284, 24);
|
||||
this.menuStrip1.TabIndex = 1;
|
||||
this.menuStrip1.Text = "menuStrip1";
|
||||
//
|
||||
// fileToolStripMenuItem
|
||||
//
|
||||
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.importMIDIToolStripMenuItem,
|
||||
this.exitToolStripMenuItem});
|
||||
this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
|
||||
this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20);
|
||||
this.fileToolStripMenuItem.Text = "&File";
|
||||
//
|
||||
// importMIDIToolStripMenuItem
|
||||
//
|
||||
this.importMIDIToolStripMenuItem.Name = "importMIDIToolStripMenuItem";
|
||||
this.importMIDIToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.I)));
|
||||
this.importMIDIToolStripMenuItem.Size = new System.Drawing.Size(184, 22);
|
||||
this.importMIDIToolStripMenuItem.Text = "&Import MIDI...";
|
||||
this.importMIDIToolStripMenuItem.Click += new System.EventHandler(this.importMIDIToolStripMenuItem_Click);
|
||||
//
|
||||
// exitToolStripMenuItem
|
||||
//
|
||||
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
|
||||
this.exitToolStripMenuItem.Size = new System.Drawing.Size(184, 22);
|
||||
this.exitToolStripMenuItem.Text = "E&xit";
|
||||
this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
|
||||
//
|
||||
// copyToolStripMenuItem
|
||||
//
|
||||
this.copyToolStripMenuItem.Name = "copyToolStripMenuItem";
|
||||
this.copyToolStripMenuItem.Size = new System.Drawing.Size(47, 20);
|
||||
this.copyToolStripMenuItem.Text = "&Copy";
|
||||
this.copyToolStripMenuItem.Click += new System.EventHandler(this.copyToolStripMenuItem_Click);
|
||||
//
|
||||
// importDlg
|
||||
//
|
||||
this.importDlg.Filter = "MIDI File|*.midi;*.mid";
|
||||
//
|
||||
// halpToolStripMenuItem
|
||||
//
|
||||
this.halpToolStripMenuItem.Name = "halpToolStripMenuItem";
|
||||
this.halpToolStripMenuItem.Size = new System.Drawing.Size(44, 20);
|
||||
this.halpToolStripMenuItem.Text = "&Halp";
|
||||
this.halpToolStripMenuItem.Click += new System.EventHandler(this.halpToolStripMenuItem_Click);
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(284, 264);
|
||||
this.Controls.Add(this.OutputTxt);
|
||||
this.Controls.Add(this.menuStrip1);
|
||||
this.MainMenuStrip = this.menuStrip1;
|
||||
this.Name = "Form1";
|
||||
this.Text = "MIDI2Piano";
|
||||
this.menuStrip1.ResumeLayout(false);
|
||||
this.menuStrip1.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.TextBox OutputTxt;
|
||||
private System.Windows.Forms.MenuStrip menuStrip1;
|
||||
private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem importMIDIToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
|
||||
private System.Windows.Forms.OpenFileDialog importDlg;
|
||||
private System.Windows.Forms.ToolStripMenuItem copyToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem halpToolStripMenuItem;
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Sanford.Multimedia;
|
||||
using Sanford.Multimedia.Midi;
|
||||
|
||||
namespace midi2piano
|
||||
{
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
|
||||
public Form1()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
struct PNote
|
||||
{
|
||||
public float Length;
|
||||
public string Note;
|
||||
|
||||
public PNote(float length, string note)
|
||||
{
|
||||
Length = length;
|
||||
Note = note;
|
||||
}
|
||||
|
||||
public static readonly PNote Default = new PNote(0, "");
|
||||
}
|
||||
|
||||
private void importMIDIToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (importDlg.ShowDialog(this)
|
||||
== System.Windows.Forms.DialogResult.Cancel)
|
||||
return;
|
||||
|
||||
List<PNote> notes = new List<PNote>();
|
||||
PNote curNote = PNote.Default;
|
||||
float tempo = 1;
|
||||
float timeSig = 4f;
|
||||
|
||||
// first, we pull midi data
|
||||
Sequence s = new Sequence(importDlg.FileName);
|
||||
|
||||
// quickly see if there's a piano track first
|
||||
// and get the tempo as well
|
||||
int piano = -1;
|
||||
for (int it = 0; it < s.Count; it++)
|
||||
{
|
||||
Track t = s[it];
|
||||
foreach (MidiEvent me in t.Iterator())
|
||||
{
|
||||
switch (me.MidiMessage.MessageType)
|
||||
{
|
||||
case MessageType.Channel:
|
||||
{
|
||||
ChannelMessage m = (ChannelMessage)me.MidiMessage;
|
||||
if (m.Command == ChannelCommand.ProgramChange)
|
||||
if ((GeneralMidiInstrument)m.Data1 == GeneralMidiInstrument.AcousticGrandPiano)
|
||||
{
|
||||
piano = it;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MessageType.Meta:
|
||||
{
|
||||
MetaMessage m = (MetaMessage)me.MidiMessage;
|
||||
if (m.MetaType == MetaType.Tempo)
|
||||
tempo = (new TempoChangeBuilder(m)).Tempo;
|
||||
else if (m.MetaType == MetaType.TimeSignature)
|
||||
timeSig = new TimeSignatureBuilder(m).Denominator;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (piano >= 0)
|
||||
break;
|
||||
}
|
||||
if (piano >= 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// didn't find one, so just try 0th track anyway
|
||||
if (piano == -1)
|
||||
piano = 0;
|
||||
|
||||
// now, pull all notes (and tempo)
|
||||
// and make sure it's a channel that has content
|
||||
for (int it = piano; it < s.Count; it++)
|
||||
{
|
||||
Track t = s[it];
|
||||
|
||||
int delta = 0;
|
||||
foreach (MidiEvent me in t.Iterator())
|
||||
{
|
||||
delta += me.DeltaTicks;
|
||||
|
||||
switch (me.MidiMessage.MessageType)
|
||||
{
|
||||
case MessageType.Channel:
|
||||
{
|
||||
ChannelMessage m = (ChannelMessage)me.MidiMessage;
|
||||
switch (m.Command)
|
||||
{
|
||||
case ChannelCommand.NoteOn:
|
||||
if (curNote.Note != "")
|
||||
{
|
||||
curNote.Length = delta / 1000F;
|
||||
delta = 0;
|
||||
notes.Add(curNote);
|
||||
}
|
||||
|
||||
curNote.Note = note2Piano(m.Data1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MessageType.Meta:
|
||||
{
|
||||
MetaMessage m = (MetaMessage)me.MidiMessage;
|
||||
if (m.MetaType == MetaType.Tempo)
|
||||
tempo = (new TempoChangeBuilder(m)).Tempo;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we get last note
|
||||
if (curNote.Note != "")
|
||||
{
|
||||
curNote.Length = delta / 1000F;
|
||||
notes.Add(curNote);
|
||||
}
|
||||
|
||||
// we found a track with content!
|
||||
if (notes.Count > 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// compress redundant accidentals/octaves
|
||||
char[] notemods = new char[7];
|
||||
int[] noteocts = new int[7];
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
notemods[i] = 'n';
|
||||
noteocts[i] = 3;
|
||||
}
|
||||
for (int i = 0; i < notes.Count; i++)
|
||||
{
|
||||
string noteStr = notes[i].Note;
|
||||
int cur_note = noteStr[0] - 0x41;
|
||||
char mod = noteStr[1];
|
||||
int oct = int.Parse(noteStr.Substring(2));
|
||||
|
||||
noteStr = noteStr.Substring(0, 1);
|
||||
if (mod != notemods[cur_note])
|
||||
{
|
||||
noteStr += new string(mod, 1);
|
||||
notemods[cur_note] = mod;
|
||||
}
|
||||
if (oct != noteocts[cur_note])
|
||||
{
|
||||
noteStr += oct.ToString();
|
||||
noteocts[cur_note] = oct;
|
||||
}
|
||||
|
||||
notes[i] = new PNote(notes[i].Length, noteStr);
|
||||
}
|
||||
|
||||
// now, we find what the "beat" length should be,
|
||||
// by counting numbers of times for each length, and finding statistical mode
|
||||
Dictionary<float, int> scores = new Dictionary<float, int>();
|
||||
foreach (PNote n in notes)
|
||||
{
|
||||
if (n.Length != 0)
|
||||
if (scores.Keys.Contains(n.Length))
|
||||
scores[n.Length]++;
|
||||
else
|
||||
scores.Add(n.Length, 1);
|
||||
}
|
||||
float winner = 1;
|
||||
int score = 0;
|
||||
foreach (KeyValuePair<float, int> kv in scores)
|
||||
{
|
||||
if (kv.Value > score)
|
||||
{
|
||||
winner = kv.Key;
|
||||
score = kv.Value;
|
||||
}
|
||||
}
|
||||
// realign all of them to match beat length
|
||||
for (int i = 0; i < notes.Count; i++)
|
||||
{
|
||||
notes[i] = new PNote(notes[i].Length / winner, notes[i].Note);
|
||||
}
|
||||
|
||||
// compress chords down
|
||||
for (int i = 0; i < notes.Count; i++)
|
||||
{
|
||||
if (notes[i].Length == 0 && i < notes.Count - 1)
|
||||
{
|
||||
notes[i + 1] = new PNote(notes[i + 1].Length, notes[i].Note + "-" + notes[i + 1].Note);
|
||||
notes.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// add in time
|
||||
for (int i = 0; i < notes.Count; i++)
|
||||
{
|
||||
float len = notes[i].Length;
|
||||
notes[i] = new PNote(len, notes[i].Note + (len != 1 ? "/" + (1 / len).ToString("0.##") : ""));
|
||||
}
|
||||
|
||||
// what is the bpm, anyway?
|
||||
int rpm = (int)(28800000 / tempo / winner); // 60 * 1,000,000 * .48 the .48 is because note lengths for some reason midi makes the beat note be .48 long
|
||||
|
||||
// now, output!
|
||||
string line = "";
|
||||
string output = "";
|
||||
int lineCount = 1;
|
||||
foreach (PNote n in notes)
|
||||
{
|
||||
if (line.Length + n.Note.Length + 1 > 51)
|
||||
{
|
||||
output += line.Substring(0, line.Length - 1) + "\r\n";
|
||||
line = "";
|
||||
if (lineCount == 50)
|
||||
break;
|
||||
lineCount++;
|
||||
}
|
||||
line += n.Note + ",";
|
||||
}
|
||||
if (line.Length > 0)
|
||||
output += line.Substring(0, line.Length - 1);
|
||||
OutputTxt.Text = "BPM: " + rpm.ToString() + "\r\n" + output;
|
||||
OutputTxt.SelectAll();
|
||||
}
|
||||
|
||||
public enum NoteNames
|
||||
{
|
||||
C = 0,
|
||||
D = 2,
|
||||
E = 4,
|
||||
F = 5,
|
||||
G = 7,
|
||||
A = 9,
|
||||
B = 11
|
||||
}
|
||||
|
||||
string note2Piano(int n)
|
||||
{
|
||||
string name, arg, octave;
|
||||
name = Enum.GetName(typeof(NoteNames), (NoteNames)(n % 12));
|
||||
if (name == null)
|
||||
{
|
||||
name = Enum.GetName(typeof(NoteNames), (NoteNames)((n + 1) % 12));
|
||||
arg = "b";
|
||||
}
|
||||
else
|
||||
{
|
||||
arg = "n";
|
||||
}
|
||||
octave = (n / 12 - 1).ToString();
|
||||
|
||||
return name + arg + octave;
|
||||
}
|
||||
|
||||
private void copyToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
OutputTxt.SelectAll();
|
||||
OutputTxt.Copy();
|
||||
}
|
||||
|
||||
private void halpToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
"This program prefers MIDIs that have a single track, otherwise it picks the first piano track it finds, else the first track. Songs with odd tempos may have their BPM's calculated wrong.",
|
||||
"Halp", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="importDlg.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>132, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("midi2piano")]
|
||||
[assembly: AssemblyProduct("midi2piano")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2011")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type. Only Windows
|
||||
// assemblies support COM.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// On Windows, the following GUID is for the ID of the typelib if this
|
||||
// project is exposed to COM. On other platforms, it unique identifies the
|
||||
// title storage container when deploying this assembly to the device.
|
||||
[assembly: Guid("9752c562-edc1-40da-8fa1-619df747e0f3")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
@@ -1,83 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>midi2piano</RootNamespace>
|
||||
<AssemblyName>midi2piano</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkProfile>Client</TargetFrameworkProfile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\x86\Debug</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;WINDOWS</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<NoStdLib>true</NoStdLib>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\x86\Release</OutputPath>
|
||||
<DefineConstants>TRACE;WINDOWS</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<NoStdLib>true</NoStdLib>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="mscorlib">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Sanford.Multimedia.Midi, Version=5.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>.\Sanford.Multimedia.Midi.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Core">
|
||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Xml.Linq">
|
||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Net">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Form1.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Form1.Designer.cs">
|
||||
<DependentUpon>Form1.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Form1.resx">
|
||||
<DependentUpon>Form1.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,31 @@
|
||||
This is a remake of 2013 midi2piano tool. Previous version should be considered obsolete.
|
||||
|
||||
Requirements:
|
||||
Python 3
|
||||
|
||||
Simply run midi2piano.py and choose midi file you want to convert.
|
||||
The "sheet music" will be copied to the clipboard.
|
||||
|
||||
There are some constants defined at the top of midi2piano.py.
|
||||
Change their value if needed.
|
||||
|
||||
LINE_LENGTH_LIM - max length of line allowed in the sheet music
|
||||
LINES_LIMIT - max amount of lines allowed in the sheet music. Extra lines will be cropped.
|
||||
|
||||
OVERALL_IMPORT_LIM - max amount of characters allowed in the sheet music.
|
||||
|
||||
|
||||
You can also transpose music if you need to
|
||||
OCTAVE_TRANSPOSE - amount of octaves you melody will be shifted by
|
||||
FLOAT_PRECISION - read comment
|
||||
|
||||
Additional notes:
|
||||
1. Unlike previous midi2piano, this tool optimizes sheet music to fit more in less lines.
|
||||
2. If two notes are less than 50 ms apart, they are chorded. BYOND works in 1/10th of a second so 50 ms is time quanta.
|
||||
4. MIDI event set_tempo is NOT supported. If your MIDI file uses set_tempo to change BPM significantly, consider using some other midi file.
|
||||
|
||||
This tool is considered final.
|
||||
|
||||
|
||||
Made by EditorRUS/Delta Epsilon from Animus Station, ss13.ru
|
||||
Contact me in Discord if you find any major issues: DeltaEpsilon#7787
|
||||
@@ -0,0 +1 @@
|
||||
from midi.midi import *
|
||||
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
This module allows user to convert MIDI melodies to SS13 sheet music ready
|
||||
for copy-and-paste
|
||||
"""
|
||||
from functools import reduce
|
||||
import midi as mi
|
||||
import easygui as egui
|
||||
import pyperclip as pclip
|
||||
|
||||
LINE_LENGTH_LIM = 50
|
||||
LINES_LIMIT = 200
|
||||
OVERALL_IMPORT_LIM = 12000
|
||||
END_OF_LINE_CHAR = """
|
||||
""" # BYOND can't parse \n and I am forced to define my own NEWLINE char
|
||||
|
||||
OCTAVE_TRANSPOSE = 0 # Change here to transpose melodies by octaves
|
||||
FLOAT_PRECISION = 2 # Change here to allow more or less numbers after dot in floats
|
||||
|
||||
OCTAVE_KEYS = 12
|
||||
HIGHEST_OCTAVE = 8
|
||||
|
||||
"""
|
||||
class Meta():
|
||||
version = 1.0
|
||||
integer = 1
|
||||
anti_integer = -1
|
||||
maximum = 1000
|
||||
epsilon = 0.51
|
||||
delta_epsilon = -0.1
|
||||
integral = []
|
||||
tensor = [[],[],[]]
|
||||
o_complexity = epsilon**2
|
||||
random_variance = 0.01
|
||||
"""
|
||||
|
||||
# UTILITY FUNCTIONS
|
||||
def condition(event):
|
||||
"""
|
||||
This function check if given MIDI event is meaningful
|
||||
"""
|
||||
if event[0] == 'track_name' and event[2] == 'Drums': # Percussion
|
||||
return False
|
||||
if event[0] == 'note': # Only thing that matters
|
||||
return True
|
||||
return False
|
||||
|
||||
def notenum2string(num, accidentals, octaves):
|
||||
"""
|
||||
This function converts given notenum to SS13 note according to previous
|
||||
runs expressed using _accidentals_ and _octaves_
|
||||
"""
|
||||
names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||
convert_table = {1:0, 3:1, 6:2, 8:3, 10:4}
|
||||
inclusion_table = {0:0, 2:1, 5:2, 7:3, 9:4}
|
||||
|
||||
num += OCTAVE_KEYS * OCTAVE_TRANSPOSE
|
||||
octave = int(num / OCTAVE_KEYS)
|
||||
if octave < 1 or octave > HIGHEST_OCTAVE:
|
||||
return ["", accidentals, octaves]
|
||||
|
||||
accidentals = accidentals.copy()
|
||||
octaves = octaves.copy()
|
||||
|
||||
output_octaves = list(octaves)
|
||||
name_indx = num % OCTAVE_KEYS
|
||||
|
||||
accidental = (len(names[name_indx]) == 2)
|
||||
output_octaves[name_indx] = octave
|
||||
add_n = False
|
||||
|
||||
if accidental:
|
||||
accidentals[convert_table[name_indx]] = True
|
||||
else:
|
||||
if name_indx in inclusion_table:
|
||||
add_n = accidentals[inclusion_table[name_indx]]
|
||||
accidentals[inclusion_table[name_indx]] = False
|
||||
|
||||
return [
|
||||
(
|
||||
names[name_indx]+
|
||||
("n" if add_n else "")+
|
||||
str((octave if octave != octaves[name_indx] else ""))
|
||||
),
|
||||
accidentals,
|
||||
output_octaves
|
||||
]
|
||||
|
||||
def dur2mod(dur, bpm_mod=1.0):
|
||||
"""
|
||||
This functions returns float representation of duration ready to be
|
||||
added to the note after /
|
||||
"""
|
||||
mod = bpm_mod / dur
|
||||
mod = round(mod, FLOAT_PRECISION)
|
||||
return str(mod).rstrip('0').rstrip('.')
|
||||
# END OF UTILITY FUNCTIONS
|
||||
|
||||
# CONVERSION FUNCTIONS
|
||||
def obtain_midi_file():
|
||||
"""
|
||||
Asks user to select MIDI and returns this file opened in binary mode for reading
|
||||
"""
|
||||
file = egui.fileopenbox(msg='Choose MIDI file to convert',
|
||||
title='MIDI file selection',
|
||||
filetypes=[['*.mid', 'MID files']])
|
||||
if not file:
|
||||
return None
|
||||
file = open(file, mode='rb').read()
|
||||
return file
|
||||
|
||||
def midi2score_without_ticks(midi_file):
|
||||
"""
|
||||
Transforms aforementioned file into a score, truncates it and returns it
|
||||
"""
|
||||
opus = mi.midi2opus(midi_file)
|
||||
opus = mi.to_millisecs(opus)
|
||||
score = mi.opus2score(opus)
|
||||
return score[1:] # Ticks don't matter anymore, it is always 1000
|
||||
|
||||
def filter_events_from_score(score):
|
||||
"""
|
||||
Filters out irrevelant events and returns new score
|
||||
"""
|
||||
return list(map( # For each score track
|
||||
lambda score_track: list(filter( # Filter irrevelant events
|
||||
condition,
|
||||
score_track
|
||||
)),
|
||||
score
|
||||
))
|
||||
|
||||
def filter_empty_tracks(score):
|
||||
"""
|
||||
Filters out empty tracks and returns new score
|
||||
"""
|
||||
return list(filter(
|
||||
lambda score_track: score_track,
|
||||
score))
|
||||
|
||||
|
||||
def filter_start_time_and_note_num(score):
|
||||
"""
|
||||
Recreates score with only note numbers and start time of each note and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda score_track: list(map(
|
||||
lambda event: [event[1], event[4]],
|
||||
score_track)),
|
||||
score))
|
||||
|
||||
def merge_events(score):
|
||||
"""Merges all tracks together and returns new score"""
|
||||
return list(reduce(
|
||||
lambda lst1, lst2: lst1+lst2,
|
||||
score))
|
||||
|
||||
def sort_score_by_event_times(score):
|
||||
"""Sorts events by start time and returns new score"""
|
||||
return list(map(
|
||||
lambda index: score[index],
|
||||
sorted(
|
||||
list(range(len(score))),
|
||||
key=lambda indx: score[indx][0])
|
||||
))
|
||||
|
||||
def convert_into_delta_times(score):
|
||||
"""
|
||||
Transform start_time into delta_time and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda super_event: (
|
||||
[
|
||||
super_event[1][0]-super_event[0][0],
|
||||
super_event[0][1]
|
||||
]), # [ [1, 2], [3, 4] ] -> [ [2, 2] ]
|
||||
zip(score[:-1], score[1:]) # Shifted association. [1, 2, 3] -> [ (1, 2), (2, 3) ]
|
||||
))+[[1000, score[-1][1]]] # Add 1 second note to the end
|
||||
|
||||
def perform_roundation(score):
|
||||
"""
|
||||
Rounds delta times to the nearest multiple of 100 ms as BYOND can't
|
||||
process duration less than that and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda event: [100*round(event[0]/100), event[1]],
|
||||
score))
|
||||
|
||||
def obtain_common_duration(score):
|
||||
"""
|
||||
Returns the most frequent duration throughout the whole melody
|
||||
"""
|
||||
# Parse durations and filter out 0s
|
||||
durs = list(filter(lambda x: x, list(map(lambda event: event[0], score))))
|
||||
unique_durs = []
|
||||
for dur in durs:
|
||||
if dur not in unique_durs:
|
||||
unique_durs.append(dur)
|
||||
# How many such durations occur throughout the melody?
|
||||
counter = [durs.count(dur) for dur in unique_durs]
|
||||
highest_counter = max(counter) # Highest counter
|
||||
dur_n_count = list(zip(durs, counter))
|
||||
dur_n_count = list(filter(lambda e: e[1] == highest_counter, dur_n_count))
|
||||
return dur_n_count[0][0] # Will be there
|
||||
|
||||
def reduce_score_to_chords(score):
|
||||
"""
|
||||
Reforms score into a chord-duration list:
|
||||
[[chord_notes], duration_of_chord]
|
||||
and returns it
|
||||
"""
|
||||
new_score = []
|
||||
new_chord = [[], 0]
|
||||
# [ [chord notes], duration of chord ]
|
||||
for event in score:
|
||||
new_chord[0].append(event[1]) # Append new note to the chord
|
||||
if event[0] == 0:
|
||||
continue # Add new notes to the chord until non-zero duration is hit
|
||||
new_chord[1] = event[0] # This is the duration of chord
|
||||
new_score.append(new_chord) # Append chord to the list
|
||||
new_chord = [[], 0] # Reset the chord
|
||||
return new_score
|
||||
|
||||
def obtain_sheet_music(score, most_frequent_dur):
|
||||
"""
|
||||
Returns unformated sheet music from score
|
||||
"""
|
||||
result = ""
|
||||
|
||||
octaves = [3 for i in range(12)]
|
||||
accidentals = [False for i in range(7)]
|
||||
for event in score:
|
||||
for note_indx in range(len(event[0])):
|
||||
data = notenum2string(event[0][note_indx], accidentals, octaves)
|
||||
result += data[0]
|
||||
accidentals = data[1]
|
||||
octaves = data[2]
|
||||
if note_indx != len(event[0])-1:
|
||||
result += '-'
|
||||
|
||||
if event[1] != most_frequent_dur: # Quarters are default
|
||||
result += '/'
|
||||
result += dur2mod(event[1], most_frequent_dur)
|
||||
result += ','
|
||||
|
||||
return result
|
||||
|
||||
def explode_sheet_music(sheet_music):
|
||||
"""
|
||||
Splits unformatted sheet music into formated lines of LINE_LEN_LIM
|
||||
and such and returns a list of such lines
|
||||
"""
|
||||
split_music = sheet_music.split(',')
|
||||
split_music = list(map(lambda note: note+',', split_music))
|
||||
split_list = []
|
||||
counter = 0
|
||||
line_counter = 1
|
||||
for note in split_music:
|
||||
if line_counter > LINES_LIMIT-1:
|
||||
break
|
||||
if counter+len(note) > LINE_LENGTH_LIM-2:
|
||||
last_note_num = len(split_list)-1
|
||||
split_list[last_note_num] = split_list[last_note_num].rstrip(',')
|
||||
split_list[last_note_num] += END_OF_LINE_CHAR
|
||||
counter = 0
|
||||
line_counter += 1
|
||||
split_list.append(note)
|
||||
counter += len(note)
|
||||
|
||||
return split_list
|
||||
|
||||
def finalize_sheet_music(split_music, most_frequent_dur):
|
||||
"""
|
||||
Recreates sheet music from exploded sheet music, truncates it and returns it
|
||||
"""
|
||||
sheet_music = ""
|
||||
for note in split_music:
|
||||
sheet_music += note
|
||||
sheet_music = sheet_music.rstrip(',') # Trim the last ,
|
||||
sheet_music = "BPM: " + str(int(60000 / most_frequent_dur)) + END_OF_LINE_CHAR + sheet_music
|
||||
return sheet_music[:min(len(sheet_music), OVERALL_IMPORT_LIM)]
|
||||
# END OF CONVERSION FUNCTIONS
|
||||
|
||||
def main_cycle():
|
||||
"""
|
||||
Activate the script
|
||||
"""
|
||||
while True:
|
||||
midi_file = obtain_midi_file()
|
||||
if not midi_file:
|
||||
return # Cancel
|
||||
score = midi2score_without_ticks(midi_file)
|
||||
score = filter_events_from_score(score)
|
||||
score = filter_start_time_and_note_num(score)
|
||||
score = filter_empty_tracks(score)
|
||||
score = merge_events(score)
|
||||
score = sort_score_by_event_times(score)
|
||||
score = convert_into_delta_times(score)
|
||||
score = perform_roundation(score)
|
||||
most_frequent_dur = obtain_common_duration(score)
|
||||
score = reduce_score_to_chords(score)
|
||||
sheet_music = obtain_sheet_music(score, most_frequent_dur)
|
||||
split_music = explode_sheet_music(sheet_music)
|
||||
sheet_music = finalize_sheet_music(split_music, most_frequent_dur)
|
||||
|
||||
pclip.copy(sheet_music)
|
||||
|
||||
main_cycle()
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Pyperclip
|
||||
|
||||
A cross-platform clipboard module for Python. (only handles plain text for now)
|
||||
By Al Sweigart al@inventwithpython.com
|
||||
BSD License
|
||||
|
||||
Usage:
|
||||
import pyperclip
|
||||
pyperclip.copy('The text to be copied to the clipboard.')
|
||||
spam = pyperclip.paste()
|
||||
|
||||
if not pyperclip.copy:
|
||||
print("Copy functionality unavailable!")
|
||||
|
||||
On Windows, no additional modules are needed.
|
||||
On Mac, the module uses pbcopy and pbpaste, which should come with the os.
|
||||
On Linux, install xclip or xsel via package manager. For example, in Debian:
|
||||
sudo apt-get install xclip
|
||||
|
||||
Otherwise on Linux, you will need the gtk or PyQt4 modules installed.
|
||||
|
||||
gtk and PyQt4 modules are not available for Python 3,
|
||||
and this module does not work with PyGObject yet.
|
||||
"""
|
||||
__version__ = '1.5.27'
|
||||
|
||||
import platform
|
||||
import os
|
||||
import subprocess
|
||||
from .clipboards import (init_osx_clipboard,
|
||||
init_gtk_clipboard, init_qt_clipboard,
|
||||
init_xclip_clipboard, init_xsel_clipboard,
|
||||
init_klipper_clipboard, init_no_clipboard)
|
||||
from .windows import init_windows_clipboard
|
||||
|
||||
# `import PyQt4` sys.exit()s if DISPLAY is not in the environment.
|
||||
# Thus, we need to detect the presence of $DISPLAY manually
|
||||
# and not load PyQt4 if it is absent.
|
||||
HAS_DISPLAY = os.getenv("DISPLAY", False)
|
||||
CHECK_CMD = "where" if platform.system() == "Windows" else "which"
|
||||
|
||||
|
||||
def _executable_exists(name):
|
||||
return subprocess.call([CHECK_CMD, name],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0
|
||||
|
||||
|
||||
def determine_clipboard():
|
||||
# Determine the OS/platform and set
|
||||
# the copy() and paste() functions accordingly.
|
||||
if 'cygwin' in platform.system().lower():
|
||||
# FIXME: pyperclip currently does not support Cygwin,
|
||||
# see https://github.com/asweigart/pyperclip/issues/55
|
||||
pass
|
||||
elif os.name == 'nt' or platform.system() == 'Windows':
|
||||
return init_windows_clipboard()
|
||||
if os.name == 'mac' or platform.system() == 'Darwin':
|
||||
return init_osx_clipboard()
|
||||
if HAS_DISPLAY:
|
||||
# Determine which command/module is installed, if any.
|
||||
try:
|
||||
import gtk # check if gtk is installed
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return init_gtk_clipboard()
|
||||
|
||||
try:
|
||||
import PyQt4 # check if PyQt4 is installed
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return init_qt_clipboard()
|
||||
|
||||
if _executable_exists("xclip"):
|
||||
return init_xclip_clipboard()
|
||||
if _executable_exists("xsel"):
|
||||
return init_xsel_clipboard()
|
||||
if _executable_exists("klipper") and _executable_exists("qdbus"):
|
||||
return init_klipper_clipboard()
|
||||
|
||||
return init_no_clipboard()
|
||||
|
||||
|
||||
def set_clipboard(clipboard):
|
||||
global copy, paste
|
||||
|
||||
clipboard_types = {'osx': init_osx_clipboard,
|
||||
'gtk': init_gtk_clipboard,
|
||||
'qt': init_qt_clipboard,
|
||||
'xclip': init_xclip_clipboard,
|
||||
'xsel': init_xsel_clipboard,
|
||||
'klipper': init_klipper_clipboard,
|
||||
'windows': init_windows_clipboard,
|
||||
'no': init_no_clipboard}
|
||||
|
||||
copy, paste = clipboard_types[clipboard]()
|
||||
|
||||
|
||||
copy, paste = determine_clipboard()
|
||||
|
||||
__all__ = ["copy", "paste"]
|
||||
@@ -0,0 +1,134 @@
|
||||
import sys
|
||||
import subprocess
|
||||
from .exceptions import PyperclipException
|
||||
|
||||
EXCEPT_MSG = """
|
||||
Pyperclip could not find a copy/paste mechanism for your system.
|
||||
For more information, please visit https://pyperclip.readthedocs.org """
|
||||
PY2 = sys.version_info[0] == 2
|
||||
text_type = unicode if PY2 else str
|
||||
|
||||
|
||||
def init_osx_clipboard():
|
||||
def copy_osx(text):
|
||||
p = subprocess.Popen(['pbcopy', 'w'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_osx():
|
||||
p = subprocess.Popen(['pbpaste', 'r'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_osx, paste_osx
|
||||
|
||||
|
||||
def init_gtk_clipboard():
|
||||
import gtk
|
||||
|
||||
def copy_gtk(text):
|
||||
global cb
|
||||
cb = gtk.Clipboard()
|
||||
cb.set_text(text)
|
||||
cb.store()
|
||||
|
||||
def paste_gtk():
|
||||
clipboardContents = gtk.Clipboard().wait_for_text()
|
||||
# for python 2, returns None if the clipboard is blank.
|
||||
if clipboardContents is None:
|
||||
return ''
|
||||
else:
|
||||
return clipboardContents
|
||||
|
||||
return copy_gtk, paste_gtk
|
||||
|
||||
|
||||
def init_qt_clipboard():
|
||||
# $DISPLAY should exist
|
||||
from PyQt4.QtGui import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
def copy_qt(text):
|
||||
cb = app.clipboard()
|
||||
cb.setText(text)
|
||||
|
||||
def paste_qt():
|
||||
cb = app.clipboard()
|
||||
return text_type(cb.text())
|
||||
|
||||
return copy_qt, paste_qt
|
||||
|
||||
|
||||
def init_xclip_clipboard():
|
||||
def copy_xclip(text):
|
||||
p = subprocess.Popen(['xclip', '-selection', 'c'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_xclip():
|
||||
p = subprocess.Popen(['xclip', '-selection', 'c', '-o'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_xclip, paste_xclip
|
||||
|
||||
|
||||
def init_xsel_clipboard():
|
||||
def copy_xsel(text):
|
||||
p = subprocess.Popen(['xsel', '-b', '-i'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_xsel():
|
||||
p = subprocess.Popen(['xsel', '-b', '-o'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_xsel, paste_xsel
|
||||
|
||||
|
||||
def init_klipper_clipboard():
|
||||
def copy_klipper(text):
|
||||
p = subprocess.Popen(
|
||||
['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents',
|
||||
text.encode('utf-8')],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=None)
|
||||
|
||||
def paste_klipper():
|
||||
p = subprocess.Popen(
|
||||
['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
# Workaround for https://bugs.kde.org/show_bug.cgi?id=342874
|
||||
# TODO: https://github.com/asweigart/pyperclip/issues/43
|
||||
clipboardContents = stdout.decode('utf-8')
|
||||
# even if blank, Klipper will append a newline at the end
|
||||
assert len(clipboardContents) > 0
|
||||
# make sure that newline is there
|
||||
assert clipboardContents.endswith('\n')
|
||||
if clipboardContents.endswith('\n'):
|
||||
clipboardContents = clipboardContents[:-1]
|
||||
return clipboardContents
|
||||
|
||||
return copy_klipper, paste_klipper
|
||||
|
||||
|
||||
def init_no_clipboard():
|
||||
class ClipboardUnavailable(object):
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise PyperclipException(EXCEPT_MSG)
|
||||
|
||||
if PY2:
|
||||
def __nonzero__(self):
|
||||
return False
|
||||
else:
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
return ClipboardUnavailable(), ClipboardUnavailable()
|
||||
@@ -0,0 +1,11 @@
|
||||
import ctypes
|
||||
|
||||
|
||||
class PyperclipException(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class PyperclipWindowsException(PyperclipException):
|
||||
def __init__(self, message):
|
||||
message += " (%s)" % ctypes.WinError()
|
||||
super(PyperclipWindowsException, self).__init__(message)
|
||||
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
This module implements clipboard handling on Windows using ctypes.
|
||||
"""
|
||||
import time
|
||||
import contextlib
|
||||
import ctypes
|
||||
from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar
|
||||
from .exceptions import PyperclipWindowsException
|
||||
|
||||
|
||||
class CheckedCall(object):
|
||||
def __init__(self, f):
|
||||
super(CheckedCall, self).__setattr__("f", f)
|
||||
|
||||
def __call__(self, *args):
|
||||
ret = self.f(*args)
|
||||
if not ret and get_errno():
|
||||
raise PyperclipWindowsException("Error calling " + self.f.__name__)
|
||||
return ret
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
setattr(self.f, key, value)
|
||||
|
||||
|
||||
def init_windows_clipboard():
|
||||
from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND,
|
||||
HINSTANCE, HMENU, BOOL, UINT, HANDLE)
|
||||
|
||||
windll = ctypes.windll
|
||||
|
||||
safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA)
|
||||
safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT,
|
||||
INT, INT, HWND, HMENU, HINSTANCE, LPVOID]
|
||||
safeCreateWindowExA.restype = HWND
|
||||
|
||||
safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow)
|
||||
safeDestroyWindow.argtypes = [HWND]
|
||||
safeDestroyWindow.restype = BOOL
|
||||
|
||||
OpenClipboard = windll.user32.OpenClipboard
|
||||
OpenClipboard.argtypes = [HWND]
|
||||
OpenClipboard.restype = BOOL
|
||||
|
||||
safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard)
|
||||
safeCloseClipboard.argtypes = []
|
||||
safeCloseClipboard.restype = BOOL
|
||||
|
||||
safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard)
|
||||
safeEmptyClipboard.argtypes = []
|
||||
safeEmptyClipboard.restype = BOOL
|
||||
|
||||
safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData)
|
||||
safeGetClipboardData.argtypes = [UINT]
|
||||
safeGetClipboardData.restype = HANDLE
|
||||
|
||||
safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData)
|
||||
safeSetClipboardData.argtypes = [UINT, HANDLE]
|
||||
safeSetClipboardData.restype = HANDLE
|
||||
|
||||
safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc)
|
||||
safeGlobalAlloc.argtypes = [UINT, c_size_t]
|
||||
safeGlobalAlloc.restype = HGLOBAL
|
||||
|
||||
safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock)
|
||||
safeGlobalLock.argtypes = [HGLOBAL]
|
||||
safeGlobalLock.restype = LPVOID
|
||||
|
||||
safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock)
|
||||
safeGlobalUnlock.argtypes = [HGLOBAL]
|
||||
safeGlobalUnlock.restype = BOOL
|
||||
|
||||
GMEM_MOVEABLE = 0x0002
|
||||
CF_UNICODETEXT = 13
|
||||
|
||||
@contextlib.contextmanager
|
||||
def window():
|
||||
"""
|
||||
Context that provides a valid Windows hwnd.
|
||||
"""
|
||||
# we really just need the hwnd, so setting "STATIC"
|
||||
# as predefined lpClass is just fine.
|
||||
hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0,
|
||||
None, None, None, None)
|
||||
try:
|
||||
yield hwnd
|
||||
finally:
|
||||
safeDestroyWindow(hwnd)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def clipboard(hwnd):
|
||||
"""
|
||||
Context manager that opens the clipboard and prevents
|
||||
other applications from modifying the clipboard content.
|
||||
"""
|
||||
# We may not get the clipboard handle immediately because
|
||||
# some other application is accessing it (?)
|
||||
# We try for at least 500ms to get the clipboard.
|
||||
t = time.time() + 0.5
|
||||
success = False
|
||||
while time.time() < t:
|
||||
success = OpenClipboard(hwnd)
|
||||
if success:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
if not success:
|
||||
raise PyperclipWindowsException("Error calling OpenClipboard")
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
safeCloseClipboard()
|
||||
|
||||
def copy_windows(text):
|
||||
# This function is heavily based on
|
||||
# http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard
|
||||
with window() as hwnd:
|
||||
# http://msdn.com/ms649048
|
||||
# If an application calls OpenClipboard with hwnd set to NULL,
|
||||
# EmptyClipboard sets the clipboard owner to NULL;
|
||||
# this causes SetClipboardData to fail.
|
||||
# => We need a valid hwnd to copy something.
|
||||
with clipboard(hwnd):
|
||||
safeEmptyClipboard()
|
||||
|
||||
if text:
|
||||
# http://msdn.com/ms649051
|
||||
# If the hMem parameter identifies a memory object,
|
||||
# the object must have been allocated using the
|
||||
# function with the GMEM_MOVEABLE flag.
|
||||
count = len(text) + 1
|
||||
handle = safeGlobalAlloc(GMEM_MOVEABLE,
|
||||
count * sizeof(c_wchar))
|
||||
locked_handle = safeGlobalLock(handle)
|
||||
|
||||
ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar))
|
||||
|
||||
safeGlobalUnlock(handle)
|
||||
safeSetClipboardData(CF_UNICODETEXT, handle)
|
||||
|
||||
def paste_windows():
|
||||
with clipboard(None):
|
||||
handle = safeGetClipboardData(CF_UNICODETEXT)
|
||||
if not handle:
|
||||
# GetClipboardData may return NULL with errno == NO_ERROR
|
||||
# if the clipboard is empty.
|
||||
# (Also, it may return a handle to an empty buffer,
|
||||
# but technically that's not empty)
|
||||
return ""
|
||||
return c_wchar_p(handle).value
|
||||
|
||||
return copy_windows, paste_windows
|
||||