diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm
index 4d2bd566d8..ec944d52d9 100644
--- a/code/__DEFINES/flags.dm
+++ b/code/__DEFINES/flags.dm
@@ -146,3 +146,11 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204
//alternate appearance flags
#define AA_TARGET_SEE_APPEARANCE (1<<0)
#define AA_MATCH_TARGET_OVERLAYS (1<<1)
+
+// spell rarities
+#define SPELL_COMMON (1 << 0)
+#define SPELL_UNCOMMON (1 << 1)
+#define SPELL_RARE (1 << 2)
+#define SPELL_EPIC (1 << 3)
+#define SPELL_LEGENDARY (1 << 4)
+#define SPELL_ALL (SPELL_COMMON | SPELL_UNCOMMON | SPELL_RARE | SPELL_EPIC | SPELL_LEGENDARY)
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index e39bd59cb1..0031c75867 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -235,6 +235,7 @@ GLOBAL_LIST_INIT(ghost_accs_options, list(GHOST_ACCS_NONE, GHOST_ACCS_DIR, GHOST
#define GHOST_MAX_VIEW_RANGE_DEFAULT 10
#define GHOST_MAX_VIEW_RANGE_MEMBER 14
+GLOBAL_LIST_INIT(learnables, list())
GLOBAL_LIST_INIT(ghost_others_options, list(GHOST_OTHERS_SIMPLE, GHOST_OTHERS_DEFAULT_SPRITE, GHOST_OTHERS_THEIR_SETTING)) //Same as ghost_accs_options.
diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm
index 3a51204f30..8cc07c9721 100644
--- a/code/__HELPERS/global_lists.dm
+++ b/code/__HELPERS/global_lists.dm
@@ -74,6 +74,8 @@
for(var/god in subtypesof(/datum/patrongods))
var/datum/patrongods/A = new god()
GLOB.patronlist[A.name] = A
+
+ GLOB.learnables = Get_Learnable_Spells()
//creates every subtype of prototype (excluding prototype) and adds it to list L.
//if no list/L is provided, one is created.
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index c935879266..add6d4f01f 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -41,6 +41,14 @@
else if(dx<0)
.+=360
+/proc/Get_Learnable_Spells()
+ var/ret = list()
+ for(var/S in GLOB.spells)
+ var/obj/effect/proc_holder/spell/spell = S
+ if(spell.learnable)
+ ret += spell
+ return ret
+
/proc/Get_Pixel_Angle(y, x)//for getting the angle when animating something's pixel_x and pixel_y
if(!y)
return (x>=0)?90:270
diff --git a/code/datums/mutations/antenna.dm b/code/datums/mutations/antenna.dm
index bc880e2661..96480e616a 100644
--- a/code/datums/mutations/antenna.dm
+++ b/code/datums/mutations/antenna.dm
@@ -53,6 +53,8 @@
name = "Mindread"
desc = ""
charge_max = 50
+ learnable = TRUE
+ rarity = 0
range = 7
clothes_req = FALSE
action_icon_state = "mindread"
diff --git a/code/game/objects/items/granters.dm b/code/game/objects/items/granters.dm
index a845359981..6cfb3d4eb2 100644
--- a/code/game/objects/items/granters.dm
+++ b/code/game/objects/items/granters.dm
@@ -438,6 +438,47 @@
drop_sound = 'sound/foley/dropsound/paper_drop.ogg'
pickup_sound = 'sound/blank.ogg'
+/obj/item/book/granter/spell/generic
+ name = "Spellbook"
+ desc = "A book of potential known only to those that can decipher its secrets."
+ icon = 'icons/obj/library.dmi'
+ icon_state = "book1"
+ oneuse = TRUE
+ drop_sound = 'sound/foley/dropsound/paper_drop.ogg'
+ pickup_sound = 'sound/blank.ogg'
+ var/obj/effect/proc_holder/spell/target = null
+
+/obj/item/book/granter/spell/generic/Initialize(loc, rarity = SPELL_ALL)
+ . = ..()
+ target = pick(GLOB.learnables)
+ var/obj/effect/proc_holder/spell/S = new target
+ spellname = S.name
+ name = "Book of [spellname]"
+
+/obj/item/book/granter/spell/generic/already_known(mob/user)
+ if(!target)
+ return TRUE
+ for(var/obj/effect/proc_holder/spell/knownspell in user.mind.spell_list)
+ if(knownspell.type == target)
+ to_chat(user,"You already know this one!")
+ return TRUE
+ return FALSE
+
+/obj/item/book/granter/spell/generic/on_reading_start(mob/user)
+ to_chat(user, "I start reading about [spellname]...")
+
+/obj/item/book/granter/spell/generic/on_reading_finished(mob/user)
+ . = ..()
+ if(oneuse)
+ name = "Siphoned Book of [target.name]"
+ desc = "A book once inscribed with magical scripture. The surface is now barren of knowledge, siphoned by someone else. It's utterly useless."
+ user.visible_message("[src] has had its magic ink ripped from the book!")
+ to_chat(user, "Your knowledge expands, you understand how to cast [spellname]!")
+ var/S = new target
+ user.mind.AddSpell(S)
+ user.log_message("learned the spell [target.name] ([target])", LOG_ATTACK, color="orange")
+ onlearned(user)
+
/obj/item/book/granter/spell/blackstone/onlearned(mob/living/carbon/user)
..()
if(oneuse == TRUE)
diff --git a/code/game/objects/items/rogueitems/books.dm b/code/game/objects/items/rogueitems/books.dm
index 0f65d45013..ee899e990d 100644
--- a/code/game/objects/items/rogueitems/books.dm
+++ b/code/game/objects/items/rogueitems/books.dm
@@ -432,6 +432,52 @@
var/qdel_source = FALSE
/obj/item/manuscript/attackby(obj/item/I, mob/living/user)
+ if(resistance_flags & ON_FIRE)
+ return ..()
+
+ if(is_blind(user))
+ return ..()
+
+ if(istype(I, /obj/item/natural/feather/magic))
+ var/qualifying = 0
+ var/exp_gain = 0
+ var/obj/item/natural/feather/magic/F = I
+
+ if(prob(5)) // They rolled a nat20
+ qualifying = SPELL_EPIC | SPELL_LEGENDARY
+ exp_gain = 100
+ to_chat(user, "You feel your feather moving with a life of its own, as you struggle to comprehend what you write.")
+ else
+ var/skill_score = 0
+ var/spell_score = 0
+ if(/datum/skill/magic/arcane in user.mind.known_skills)
+ skill_score = user.mind.known_skills[/datum/skill/magic/arcane] * 0.1
+ spell_score = number_of_pages * 10 + skill_score
+ if(spell_score >= 10) // No, this doesn't make sense right now. I'm going to sleep on it
+ qualifying |= SPELL_COMMON // and figure out how to balance this thing.
+ if(spell_score >= 50)
+ qualifying |= SPELL_UNCOMMON
+ if(spell_score >= 95)
+ qualifying |= SPELL_RARE
+ if(spell_score >= 105)
+ qualifying |= SPELL_EPIC
+ if(spell_score >= 115)
+ qualifying |= SPELL_LEGENDARY
+
+ if(qualifying)
+ var/obj/item/book/granter/spell/generic/SB = new /obj/item/book/granter/spell/generic(get_turf(I.loc), qualifying)
+ if(user.Adjacent(SB))
+ SB.add_fingerprint(user)
+ user.put_in_hands(SB)
+ if(exp_gain)
+ user.mind.adjust_experience(/datum/skill/magic/arcane, exp_gain)
+ to_chat(user, "As you finish writing, the feather glows and envelops the manuscript, becoming a new spellbook.")
+ // else something here about the feather not being able to write a spellbook
+ if(F.uses >= F.max_uses)
+ to_chat(user, "The feather's magic glows dimly, and then it turns into dust.")
+ new /obj/item/ash(get_turf(user.loc))
+ qdel(F)
+ return
// why is a book crafting kit using the craft system, but crafting a book isn't? Well the crafting system for *some reason* is made in such a way as to make reworking it to allow you to put reqs vars in the crafted item near *impossible.*
if(istype(I, /obj/item/book_crafting_kit))
var/obj/item/book/rogue/playerbook/PB = new /obj/item/book/rogue/playerbook(get_turf(I.loc), TRUE, user, compiled_pages)
diff --git a/code/game/objects/items/rogueitems/natural/feather.dm b/code/game/objects/items/rogueitems/natural/feather.dm
index cc0517ed75..03392025d7 100644
--- a/code/game/objects/items/rogueitems/natural/feather.dm
+++ b/code/game/objects/items/rogueitems/natural/feather.dm
@@ -15,4 +15,12 @@
max_integrity = 20
muteinmouth = TRUE
spitoutmouth = FALSE
- w_class = WEIGHT_CLASS_TINY
\ No newline at end of file
+ w_class = WEIGHT_CLASS_TINY
+
+/obj/item/natural/feather/magic
+ name = "magic feather"
+ color = "#ffee00"
+ desc = "A fluffy feather that seems to shimmer with a faint magical aura."
+ var/uses = 0
+ var/max_uses = 3 // TODO: Balance this
+
\ No newline at end of file
diff --git a/code/modules/roguetown/roguecrafting/items.dm b/code/modules/roguetown/roguecrafting/items.dm
index fd96f30b41..4f29ed8afd 100644
--- a/code/modules/roguetown/roguecrafting/items.dm
+++ b/code/modules/roguetown/roguecrafting/items.dm
@@ -238,3 +238,11 @@
/obj/item/natural/cloth = 1)
tools = list(/obj/item/needle = 1)
req_table = TRUE
+
+/datum/crafting_recipe/roguetown/magic_feather
+ name = "magic feather"
+ result = /obj/item/natural/feather/magic
+ reqs = list(
+ /obj/item/reagent_containers/powder = 1,
+ /obj/item/natural/feather = 1
+ )
diff --git a/code/modules/spells/roguetown/necromancer.dm b/code/modules/spells/roguetown/necromancer.dm
index c7110d75e8..a6b46e06a9 100644
--- a/code/modules/spells/roguetown/necromancer.dm
+++ b/code/modules/spells/roguetown/necromancer.dm
@@ -4,6 +4,8 @@
releasedrain = 30
chargetime = 5
range = 7
+ learnable = TRUE
+ rarity = 0
warnie = "sydwarning"
movement_interrupt = FALSE
chargedloop = null
@@ -37,6 +39,8 @@
overlay_state = "raiseskele"
releasedrain = 30
chargetime = 15
+ learnable = TRUE
+ rarity = 0
range = 7
warnie = "sydwarning"
movement_interrupt = FALSE
@@ -63,6 +67,8 @@
desc = ""
clothes_req = FALSE
range = 7
+ learnable = TRUE
+ rarity = 0
overlay_state = "raiseskele"
sound = list('sound/magic/magnet.ogg')
releasedrain = 40
@@ -86,6 +92,8 @@
name = "Ray of Sickness"
desc = ""
clothes_req = FALSE
+ learnable = TRUE
+ rarity = 0
range = 15
projectile_type = /obj/projectile/magic/sickness
overlay_state = "raiseskele"
diff --git a/code/modules/spells/roguetown/wizard.dm b/code/modules/spells/roguetown/wizard.dm
index 1b31638f8b..07d5823bed 100644
--- a/code/modules/spells/roguetown/wizard.dm
+++ b/code/modules/spells/roguetown/wizard.dm
@@ -3,6 +3,8 @@
name = "Bolt of Lightning"
desc = ""
clothes_req = FALSE
+ learnable = TRUE
+ rarity = 0
overlay_state = "lightning"
sound = 'sound/magic/lightning.ogg'
range = 8
@@ -154,6 +156,8 @@
/obj/effect/proc_holder/spell/invoked/projectile/fireball
name = "Fireball"
desc = ""
+ learnable = TRUE
+ rarity = 80
clothes_req = FALSE
range = 8
projectile_type = /obj/projectile/magic/aoe/fireball/rogue
@@ -205,6 +209,8 @@
desc = ""
clothes_req = FALSE
range = 8
+ learnable = TRUE
+ rarity = 10
projectile_type = /obj/projectile/magic/aoe/fireball/rogue/great
overlay_state = "fireball"
sound = list('sound/magic/fireball.ogg')
@@ -230,6 +236,8 @@
name = "Fetch"
desc = ""
clothes_req = FALSE
+ learnable = TRUE
+ rarity = 0
range = 15
projectile_type = /obj/projectile/magic/fetch
overlay_state = ""
diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm
index 0f19fabbe0..ad25623c54 100644
--- a/code/modules/spells/spell.dm
+++ b/code/modules/spells/spell.dm
@@ -1,7 +1,6 @@
#define TARGET_CLOSEST 1
#define TARGET_RANDOM 2
-
/obj/effect/proc_holder
var/panel = "Debug"//What panel the proc holder needs to go on.
var/active = FALSE //Used by toggle based abilities.
@@ -129,6 +128,8 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) //needed for th
opacity = 0
var/school = "evocation" //not relevant at now, but may be important later if there are changes to how spells work. the ones I used for now will probably be changed... maybe spell presets? lacking flexibility but with some other benefit?
+ var/learnable = FALSE // The spell is acquirable outside of admin intervention or through spawning. Default to "No" because many shouldn't be learned.
+ var/rarity = 0 // Percentage chance to scribe.
var/charge_type = "recharge" //can be recharge or charges, see charge_max and charge_counter descriptions; can also be based on the holder's vars now, use "holder_var" for that
diff --git a/tools/mapmerge2/requirements.txt b/tools/mapmerge2/requirements.txt
index fad43483a7..d24cb40dcc 100644
--- a/tools/mapmerge2/requirements.txt
+++ b/tools/mapmerge2/requirements.txt
@@ -1,3 +1,3 @@
-#pygit2==0.27.2
+pygit2==0.27.2
bidict==0.13.1
Pillow==6.2.0