diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b5c43d0173b257c65622f434c7c82d4d80304526
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,63 @@
+version: 2
+jobs:
+  build:
+    working_directory: /root/SRB2
+    docker:
+      - image: debian:jessie
+        environment:
+          CC: ccache gcc -m32
+          PKG_CONFIG_LIBDIR: /usr/lib/i386-linux-gnu/pkgconfig
+          LIBGME_CFLAGS: -I/usr/include
+          LIBGME_LDFLAGS: -lgme
+          CCACHE_COMPRESS: true
+          WFLAGS: -Wno-unsuffixed-float-constants
+          GCC49: true
+      #- image: ubuntu:trusty
+      #  environment:
+      #    CC: ccache gcc -m32
+      #    PKG_CONFIG_LIBDIR: /usr/lib/i386-linux-gnu/pkgconfig
+      #    LIBGME_CFLAGS: -I/usr/include
+      #    LIBGME_LDFLAGS: -lgme
+      #    CCACHE_COMPRESS: true
+      #    WFLAGS: -Wno-unsuffixed-float-constants
+      #    GCC48: true
+    steps:
+      - run:
+          name: Add i386 arch
+          command: dpkg --add-architecture i386
+      - run:
+          name: Update APT listing
+          command: apt-get -qq update
+      - run:
+          name: Support S3 upload
+          command: apt-get -qq -y install ca-certificates
+      - restore_cache:
+          keys:
+            - v1-SRB2-APT
+      - run:
+          name: Install SDK
+          command: apt-get -qq -y install git build-essential nasm libpng12-dev:i386 libsdl2-mixer-dev:i386 libgme-dev:i386 gettext ccache wget gcc-multilib upx
+      - save_cache:
+          key: v1-SRB2-APT
+          paths:
+            - /var/cache/apt/archives
+      - checkout
+      - run:
+          name: Clean build
+          command: make -C src LINUX=1 clean
+      - restore_cache:
+          keys:
+            - v1-SRB2-{{ .Branch }}-{{ checksum "objs/Linux/SDL/Release/depend.dep" }}
+      - run:
+          name: Compile
+          command: make -C src LINUX=1 ERRORMODE=1 -k
+      - store_artifacts:
+          path: /root/SRB2/bin/Linux/Release/
+          destination: bin
+      - save_cache:
+          key: v1-SRB2-{{ .Branch }}-{{ checksum "objs/Linux/SDL/Release/depend.dep" }}
+          paths:
+            - /root/.ccache
+
+
+
diff --git a/README.md b/README.md
index eb06156b4673c7656512ed16528b0edbbada3970..d16071454876bc178222e778ca7c20f63821bb97 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
 
 [![Build status](https://ci.appveyor.com/api/projects/status/399d4hcw9yy7hg2y?svg=true)](https://ci.appveyor.com/project/STJr/srb2)
 [![Build status](https://travis-ci.org/STJr/SRB2.svg?branch=master)](https://travis-ci.org/STJr/SRB2)
+[![CircleCI](https://circleci.com/gh/STJr/SRB2/tree/master.svg?style=svg)](https://circleci.com/gh/STJr/SRB2/tree/master)
 
 [Sonic Robo Blast 2](https://srb2.org/) is a 3D Sonic the Hedgehog fangame based on a modified version of [Doom Legacy](http://doomlegacy.sourceforge.net/).
 
diff --git a/src/dehacked.c b/src/dehacked.c
index 2658267ae96b248ce71ccf62e9001ce1f74a79a0..fa246b88c0eb5438e2d32822ca32c545cf11d38a 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -2269,6 +2269,8 @@ static void reademblemdata(MYFILE *f, INT32 num)
 					emblemlocations[num-1].type = ET_NGRADE;
 				else if (fastcmp(word2, "NTIME"))
 					emblemlocations[num-1].type = ET_NTIME;
+				else if (fastcmp(word2, "MAP"))
+					emblemlocations[num-1].type = ET_MAP;
 				else
 					emblemlocations[num-1].type = (UINT8)value;
 			}
@@ -7421,7 +7423,12 @@ struct {
 	{"SF_X8AWAYSOUND",SF_X8AWAYSOUND},
 	{"SF_NOINTERRUPT",SF_NOINTERRUPT},
 	{"SF_X2AWAYSOUND",SF_X2AWAYSOUND},
-
+	
+	// Map emblem var flags
+	{"ME_ALLEMERALDS",ME_ALLEMERALDS},
+	{"ME_ULTIMATE",ME_ULTIMATE},
+	{"ME_PERFECT",ME_PERFECT},
+	
 #ifdef HAVE_BLUA
 	// p_local.h constants
 	{"FLOATSPEED",FLOATSPEED},
diff --git a/src/g_game.c b/src/g_game.c
index 3839e0448aa800d442b0bdbb5c065cbee9b61817..0d011bd7f4f102f00ab3cbb1d82d6bc1afc8ccc4 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -1564,11 +1564,6 @@ static void Analog_OnChange(void)
 
 	// cameras are not initialized at this point
 
-	if (leveltime > 1)
-		CV_SetValue(&cv_cam_dist, 128);
-	if (cv_analog.value || demoplayback)
-		CV_SetValue(&cv_cam_dist, 192);
-
 	if (!cv_chasecam.value && cv_analog.value) {
 		CV_SetValue(&cv_analog, 0);
 		return;
@@ -1589,11 +1584,6 @@ static void Analog2_OnChange(void)
 
 	// cameras are not initialized at this point
 
-	if (leveltime > 1)
-		CV_SetValue(&cv_cam2_dist, 128);
-	if (cv_analog2.value)
-		CV_SetValue(&cv_cam2_dist, 192);
-
 	if (!cv_chasecam2.value && cv_analog2.value) {
 		CV_SetValue(&cv_analog2, 0);
 		return;
diff --git a/src/g_input.c b/src/g_input.c
index b004384c05309f3ba4b751823635d0887a55e103..a538df06cda1fc049da90892777f18da25cab994 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -25,10 +25,10 @@ static CV_PossibleValue_t mousesens_cons_t[] = {{1, "MIN"}, {MAXMOUSESENSITIVITY
 static CV_PossibleValue_t onecontrolperkey_cons_t[] = {{1, "One"}, {2, "Several"}, {0, NULL}};
 
 // mouse values are used once
-consvar_t cv_mousesens = {"mousesens", "35", CV_SAVE, mousesens_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_mousesens2 = {"mousesens2", "35", CV_SAVE, mousesens_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_mouseysens = {"mouseysens", "35", CV_SAVE, mousesens_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_mouseysens2 = {"mouseysens2", "35", CV_SAVE, mousesens_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_mousesens = {"mousesens", "12", CV_SAVE, mousesens_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_mousesens2 = {"mousesens2", "12", CV_SAVE, mousesens_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_mouseysens = {"mouseysens", "12", CV_SAVE, mousesens_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_mouseysens2 = {"mouseysens2", "12", CV_SAVE, mousesens_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_controlperkey = {"controlperkey", "One", CV_SAVE, onecontrolperkey_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 INT32 mousex, mousey;
@@ -1154,10 +1154,8 @@ void G_Controldefault(void)
 #else
 void G_Controldefault(void)
 {
-	gamecontrol[gc_forward    ][0] = KEY_UPARROW;
-	gamecontrol[gc_forward    ][1] = 'w';
-	gamecontrol[gc_backward   ][0] = KEY_DOWNARROW;
-	gamecontrol[gc_backward   ][1] = 's';
+	gamecontrol[gc_forward    ][0] = 'w';
+	gamecontrol[gc_backward   ][0] = 's';
 	gamecontrol[gc_strafeleft ][0] = 'a';
 	gamecontrol[gc_straferight][0] = 'd';
 	gamecontrol[gc_turnleft   ][0] = KEY_LEFTARROW;
@@ -1178,19 +1176,18 @@ void G_Controldefault(void)
 	gamecontrol[gc_fire       ][1] = KEY_MOUSE1+0;
 	gamecontrol[gc_firenormal ][0] = 'c';
 	gamecontrol[gc_tossflag   ][0] = '\'';
-	gamecontrol[gc_use        ][0] = 'x';
+	gamecontrol[gc_use        ][0] = KEY_LSHIFT;
 	gamecontrol[gc_camtoggle  ][0] = 'v';
 	gamecontrol[gc_camleft    ][0] = '[';
 	gamecontrol[gc_camright   ][0] = ']';
 	gamecontrol[gc_camreset   ][0] = 'r';
-	gamecontrol[gc_lookup     ][0] = KEY_PGUP;
-	gamecontrol[gc_lookdown   ][0] = KEY_PGDN;
+	gamecontrol[gc_lookup     ][0] = KEY_UPARROW;
+	gamecontrol[gc_lookdown   ][0] = KEY_DOWNARROW;
 	gamecontrol[gc_centerview ][0] = KEY_END;
 	gamecontrol[gc_talkkey    ][0] = 't';
 	gamecontrol[gc_teamkey    ][0] = 'y';
 	gamecontrol[gc_scores     ][0] = KEY_TAB;
-	gamecontrol[gc_jump       ][0] = 'z';
-	gamecontrol[gc_jump       ][1] = KEY_MOUSE1+1;
+	gamecontrol[gc_jump       ][0] = KEY_SPACE;
 	gamecontrol[gc_console    ][0] = KEY_CONSOLE;
 	gamecontrol[gc_pause      ][0] = KEY_PAUSE;
 #ifdef WMINPUT
diff --git a/src/m_cond.c b/src/m_cond.c
index 5e23d40802bfe9ef81e5560b86d8403a43a41340..7f977c15d815b73b972094b805f4a28a6f0128d7 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -929,7 +929,7 @@ UINT8 M_CheckLevelEmblems(void)
 	// Update Score, Time, Rings emblems
 	for (i = 0; i < numemblems; ++i)
 	{
-		if (emblemlocations[i].type <= ET_SKIN || emblemlocations[i].collected)
+		if (emblemlocations[i].type <= ET_SKIN || emblemlocations[i].type == ET_MAP || emblemlocations[i].collected)
 			continue;
 
 		levelnum = emblemlocations[i].level;
@@ -963,6 +963,42 @@ UINT8 M_CheckLevelEmblems(void)
 	return somethingUnlocked;
 }
 
+UINT8 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separate print when awarding emblems and it's sorta different enough.
+{
+	INT32 i;
+	INT32 embtype;
+	INT16 levelnum;
+	UINT8 res;
+	UINT8 somethingUnlocked = 0;
+	UINT8 flags;
+
+	for (i = 0; i < numemblems; ++i)
+	{
+		if (emblemlocations[i].type != ET_MAP || emblemlocations[i].collected)
+			continue;
+
+		levelnum = emblemlocations[i].level;
+		embtype = emblemlocations[i].var;
+		flags = MV_BEATEN;
+		
+		if (embtype & ME_ALLEMERALDS)
+			flags |= MV_ALLEMERALDS;
+		
+		if (embtype & ME_ULTIMATE)
+			flags |= MV_ULTIMATE;
+		
+		if (embtype & ME_PERFECT)
+			flags |= MV_PERFECT;
+		
+		res = ((mapvisited[levelnum - 1] & flags) == flags);
+		
+		emblemlocations[i].collected = res;
+		if (res)
+			++somethingUnlocked;
+	}
+	return somethingUnlocked;
+}
+
 // -------------------
 // Quick unlock checks
 // -------------------
diff --git a/src/m_cond.h b/src/m_cond.h
index e61ff1f795819c03a4ff59803e9fa836afb25088..94802f66594bfd3f2f3e7162a87b4387d66c0ace 100644
--- a/src/m_cond.h
+++ b/src/m_cond.h
@@ -73,6 +73,12 @@ typedef struct
 #define ET_RINGS  4
 #define ET_NGRADE 5
 #define ET_NTIME  6
+#define ET_MAP    7
+
+// Map emblem flags
+#define ME_ALLEMERALDS 1
+#define ME_ULTIMATE    2
+#define ME_PERFECT     4
 
 typedef struct
 {
@@ -153,6 +159,7 @@ void M_CheckUnlockConditions(void);
 UINT8 M_UpdateUnlockablesAndExtraEmblems(void);
 void M_SilentUpdateUnlockablesAndEmblems(void);
 UINT8 M_CheckLevelEmblems(void);
+UINT8 M_CompletionEmblems(void);
 
 // Checking unlockable status
 UINT8 M_AnySecretUnlocked(void);
diff --git a/src/m_menu.c b/src/m_menu.c
index 026349dfabfee2dc5d1ec74a255f16f0ee1dd4c4..6ca6355580004507b78b92291aaeff649dd030c7 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -2897,6 +2897,8 @@ static void M_DrawMapEmblems(INT32 mapnum, INT32 x, INT32 y)
 				curtype = 1; break;
 			case ET_NGRADE: case ET_NTIME:
 				curtype = 2; break;
+			case ET_MAP:
+				curtype = 3; break;
 			default:
 				curtype = 0; break;
 		}
diff --git a/src/m_misc.c b/src/m_misc.c
index d6ab40196694df21a9150ce799d596f89498b159..f8d3213c2d8b8ba7ced535c343603b37e75381e4 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -585,7 +585,7 @@ static const char *Newsnapshotfile(const char *pathname, const char *ext)
 
 		i += add * result;
 
-		if (add < 0 || add > 9999)
+		if (i < 0 || i > 9999)
 			return NULL;
 	}
 
diff --git a/src/p_setup.c b/src/p_setup.c
index 012caba78bb4c4434d1633dd57e9bf5b257dbb08..975cc600d9be737d6f83537514de38c110635c9b 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -2583,8 +2583,9 @@ boolean P_SetupLevel(boolean skipprecip)
 
 	if (!dedicated)
 	{
-		if (!cv_cam_speed.changed)
-			CV_Set(&cv_cam_speed, cv_cam_speed.defaultvalue);
+		// Salt: CV_ClearChangedFlags() messes with your settings :(
+		/*if (!cv_cam_speed.changed)
+			CV_Set(&cv_cam_speed, cv_cam_speed.defaultvalue);*/
 
 		if (!cv_chasecam.changed)
 			CV_SetValue(&cv_chasecam, chase);
@@ -2878,22 +2879,21 @@ boolean P_SetupLevel(boolean skipprecip)
 				camera.angle = FixedAngle((fixed_t)thing->angle << FRACBITS);
 			}
 		}
-
-		if (!cv_cam_height.changed)
+		
+		// Salt: CV_ClearChangedFlags() messes with your settings :(
+		/*if (!cv_cam_height.changed)
 			CV_Set(&cv_cam_height, cv_cam_height.defaultvalue);
+		if (!cv_cam2_height.changed)
+			CV_Set(&cv_cam2_height, cv_cam2_height.defaultvalue);
 
 		if (!cv_cam_dist.changed)
 			CV_Set(&cv_cam_dist, cv_cam_dist.defaultvalue);
+		if (!cv_cam2_dist.changed)
+			CV_Set(&cv_cam2_dist, cv_cam2_dist.defaultvalue);*/
 
+		// Though, I don't think anyone would care about cam_rotate being reset back to the only value that makes sense :P
 		if (!cv_cam_rotate.changed)
 			CV_Set(&cv_cam_rotate, cv_cam_rotate.defaultvalue);
-
-		if (!cv_cam2_height.changed)
-			CV_Set(&cv_cam2_height, cv_cam2_height.defaultvalue);
-
-		if (!cv_cam2_dist.changed)
-			CV_Set(&cv_cam2_dist, cv_cam2_dist.defaultvalue);
-
 		if (!cv_cam2_rotate.changed)
 			CV_Set(&cv_cam2_rotate, cv_cam2_rotate.defaultvalue);
 
diff --git a/src/p_user.c b/src/p_user.c
index bccf930ddace89a3a0f537d835cdc69961b54ebd..44077d679c42dcefac5a9940745431f21538fbe3 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -8234,18 +8234,18 @@ static CV_PossibleValue_t CV_CamSpeed[] = {{0, "MIN"}, {1*FRACUNIT, "MAX"}, {0,
 static CV_PossibleValue_t rotation_cons_t[] = {{1, "MIN"}, {45, "MAX"}, {0, NULL}};
 static CV_PossibleValue_t CV_CamRotate[] = {{-720, "MIN"}, {720, "MAX"}, {0, NULL}};
 
-consvar_t cv_cam_dist = {"cam_dist", "128", CV_FLOAT, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_cam_height = {"cam_height", "20", CV_FLOAT, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam_dist = {"cam_dist", "160", CV_FLOAT|CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam_height = {"cam_height", "25", CV_FLOAT|CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_cam_still = {"cam_still", "Off", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_cam_speed = {"cam_speed", "0.25", CV_FLOAT, CV_CamSpeed, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam_speed = {"cam_speed", "0.3", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_cam_rotate = {"cam_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate_OnChange, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_cam_rotspeed = {"cam_rotspeed", "10", 0, rotation_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_cam2_dist = {"cam2_dist", "128", CV_FLOAT, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_cam2_height = {"cam2_height", "20", CV_FLOAT, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam_rotspeed = {"cam_rotspeed", "10", CV_SAVE, rotation_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam2_dist = {"cam2_dist", "160", CV_FLOAT|CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam2_height = {"cam2_height", "25", CV_FLOAT|CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_cam2_still = {"cam2_still", "Off", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_cam2_speed = {"cam2_speed", "0.25", CV_FLOAT, CV_CamSpeed, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam2_speed = {"cam2_speed", "0.3", CV_FLOAT|CV_SAVE, CV_CamSpeed, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_cam2_rotate = {"cam2_rotate", "0", CV_CALL|CV_NOINIT, CV_CamRotate, CV_CamRotate2_OnChange, 0, NULL, NULL, 0, 0, NULL};
-consvar_t cv_cam2_rotspeed = {"cam2_rotspeed", "10", 0, rotation_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
+consvar_t cv_cam2_rotspeed = {"cam2_rotspeed", "10", CV_SAVE, rotation_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
 
 fixed_t t_cam_dist = -42;
 fixed_t t_cam_height = -42;
@@ -8503,13 +8503,15 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
 	{
 		dist = camdist;
 
+		// x1.2 dist for analog
+		if (P_AnalogMove(player))
+			dist = FixedMul(dist, 6*FRACUNIT/5);
+		
 		if (player->climbing || player->exiting || player->playerstate == PST_DEAD || (player->powers[pw_carry] == CR_ROPEHANG || player->powers[pw_carry] == CR_GENERIC || player->powers[pw_carry] == CR_MACESPIN))
 			dist <<= 1;
 	}
 
-	dist = FixedMul(dist, player->camerascale);
-
-	checkdist = dist;
+	checkdist = (dist = FixedMul(dist, player->camerascale));
 
 	if (checkdist < 128*FRACUNIT)
 		checkdist = 128*FRACUNIT;
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 2562cff666217e75e5c43be0acf065c6dbc20980..44cb991a7000fbea276aa64b1e8f07d5d1314254 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -210,7 +210,7 @@ void R_PortalClearClipSegs(INT32 start, INT32 end)
 //
 // It assumes that Doom has already ruled out a door being closed because
 // of front-back closure (e.g. front floor is taller than back ceiling).
-static inline INT32 R_DoorClosed(void)
+static INT32 R_DoorClosed(void)
 {
 	return
 
diff --git a/src/y_inter.c b/src/y_inter.c
index 3b14f28375ca3f3530a8a5d99c785d2c2a7616d2..5548fe346c88a59c9d016601483e7430519a200e 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -912,7 +912,8 @@ static void Y_UpdateRecordReplays(void)
 void Y_StartIntermission(void)
 {
 	INT32 i;
-
+	UINT8 completionEmblems = M_CompletionEmblems();
+	
 	intertic = -1;
 
 #ifdef PARANOIA
@@ -1007,6 +1008,9 @@ void Y_StartIntermission(void)
 
 				if (modeattacking == ATTACKING_RECORD)
 					Y_UpdateRecordReplays();
+				
+				if (completionEmblems)
+					CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)completionEmblems, completionEmblems > 1 ? "s" : "");
 			}
 
 			for (i = 0; i < 4; ++i)
@@ -1106,6 +1110,10 @@ void Y_StartIntermission(void)
 			{
 				if (!stagefailed)
 					mapvisited[gamemap-1] |= MV_BEATEN;
+				
+				// all emeralds/ultimate/perfect emblems won't be possible in ss, oh well?
+				if (completionEmblems)
+					CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)completionEmblems, completionEmblems > 1 ? "s" : "");
 			}
 
 			// give out ring bonuses