diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c8e62c3a4..056897f40 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -998,6 +998,7 @@ set (PCH_SOURCES
 	s_sound.cpp
 	serializer.cpp
 	sc_man.cpp
+	scriptutil.cpp
 	st_stuff.cpp
 	statistics.cpp
 	stats.cpp
diff --git a/src/fragglescript/t_func.cpp b/src/fragglescript/t_func.cpp
index c034b5116..be6c18520 100644
--- a/src/fragglescript/t_func.cpp
+++ b/src/fragglescript/t_func.cpp
@@ -48,6 +48,7 @@
 #include "r_utility.h"
 #include "g_levellocals.h"
 #include "actorinlines.h"
+#include "scriptutil.h"
 
 static FRandom pr_script("FScript");
 
@@ -283,6 +284,17 @@ static int T_GetPlayerNum(const svalue_t &arg)
 	return playernum;
 }
 
+APlayerPawn *T_GetPlayerActor(const svalue_t &arg)
+{
+	int num = T_GetPlayerNum(arg);
+	return num == -1 ? nullptr : players[num].mo;
+}
+
+PClassActor *T_ClassType(const svalue_t &arg)
+{
+	return PClass::FindActor(stringvalue(arg));
+}
+
 //==========================================================================
 //
 // Finds a sector from a tag. This has been extended to allow looking for
@@ -2834,34 +2846,8 @@ void FParser::SF_SetWeapon()
 {
 	if (CheckArgs(2))
 	{
-		int playernum=T_GetPlayerNum(t_argv[0]);
-		if (playernum!=-1) 
-		{
-			AInventory *item = players[playernum].mo->FindInventory (PClass::FindActor (stringvalue(t_argv[1])));
-
-			if (item == NULL || !item->IsKindOf(NAME_Weapon))
-			{
-			}
-			else if (players[playernum].ReadyWeapon == item)
-			{
-				// The weapon is already selected, so setweapon succeeds by default,
-				// but make sure the player isn't switching away from it.
-				players[playernum].PendingWeapon = WP_NOCHANGE;
-				t_return.value.i = 1;
-			}
-			else
-			{
-				auto weap = static_cast<AWeapon *> (item);
-
-				if (weap->CheckAmmo (AWeapon::EitherFire, false))
-				{
-					// There's enough ammo, so switch to it.
-					t_return.value.i = 1;
-					players[playernum].PendingWeapon = weap;
-				}
-			}
-		}
-		t_return.value.i = 0;
+		t_return.type = svt_int;
+		t_return.value.i = ScriptUtil::Exec(NAME_SetWeapon, ScriptUtil::Pointer, T_GetPlayerActor(t_argv[0]), ScriptUtil::Class, T_ClassType(t_argv[1]), ScriptUtil::End);
 	}
 }
 
diff --git a/src/namedef.h b/src/namedef.h
index 2d0c2c126..6dc22c113 100644
--- a/src/namedef.h
+++ b/src/namedef.h
@@ -983,3 +983,8 @@ xx(snd_output)
 xx(snd_output_format)
 xx(snd_speakermode)
 xx(snd_resampler)
+
+// ScriptUtil entry points
+xx(ScriptUtil)
+xx(SetMarineWeapon)
+xx(SetMarineSprite)
diff --git a/src/p_acs.cpp b/src/p_acs.cpp
index 22ada7712..b90558632 100644
--- a/src/p_acs.cpp
+++ b/src/p_acs.cpp
@@ -74,6 +74,7 @@
 #include "g_levellocals.h"
 #include "actorinlines.h"
 #include "types.h"
+#include "scriptutil.h"
 
 	// P-codes for ACS scripts
 	enum
@@ -6963,28 +6964,6 @@ static bool CharArrayParms(int &capacity, int &offset, int &a, FACSStackMemory&
 	return true;
 }
 
-static void SetMarineWeapon(AActor *marine, int weapon)
-{
-	static VMFunction *smw = nullptr;
-	if (smw == nullptr) PClass::FindFunction(&smw, NAME_ScriptedMarine, NAME_SetWeapon);
-	if (smw)
-	{
-		VMValue params[2] = { marine, weapon };
-		VMCall(smw, params, 2, nullptr, 0);
-	}
-}
-
-static void SetMarineSprite(AActor *marine, PClassActor *source)
-{
-	static VMFunction *sms = nullptr;
-	if (sms == nullptr) PClass::FindFunction(&sms, NAME_ScriptedMarine, NAME_SetSprite);
-	if (sms)
-	{
-		VMValue params[2] = { marine, source };
-		VMCall(sms, params, 2, nullptr, 0);
-	}
-}
-
 int DLevelScript::RunScript ()
 {
 	DACSThinker *controller = DACSThinker::ActiveThinker;
@@ -9832,93 +9811,16 @@ scriptwait:
             break;
 
 		case PCD_SETWEAPON:
-			if (activator == NULL || activator->player == NULL)
-			{
-				STACK(1) = 0;
-			}
-			else
-			{
-				AInventory *item = activator->FindInventory (PClass::FindActor (FBehavior::StaticLookupString (STACK(1))));
-
-				if (item == NULL || !item->IsKindOf(NAME_Weapon))
-				{
-					STACK(1) = 0;
-				}
-				else if (activator->player->ReadyWeapon == item)
-				{
-					// The weapon is already selected, so setweapon succeeds by default,
-					// but make sure the player isn't switching away from it.
-					activator->player->PendingWeapon = WP_NOCHANGE;
-					STACK(1) = 1;
-				}
-				else
-				{
-					AWeapon *weap = static_cast<AWeapon *> (item);
-
-					if (weap->CheckAmmo (AWeapon::EitherFire, false))
-					{
-						// There's enough ammo, so switch to it.
-						STACK(1) = 1;
-						activator->player->PendingWeapon = weap;
-					}
-					else
-					{
-						STACK(1) = 0;
-					}
-				}
-			}
+			STACK(1) = ScriptUtil::Exec(NAME_SetWeapon, ScriptUtil::Pointer, activator, ScriptUtil::ACSClass, STACK(1), ScriptUtil::End);
 			break;
 
 		case PCD_SETMARINEWEAPON:
-			if (STACK(2) != 0)
-			{
-				AActor *marine;
-				NActorIterator iterator(NAME_ScriptedMarine, STACK(2));
-
-				while ((marine = iterator.Next()) != NULL)
-				{
-					SetMarineWeapon(marine, STACK(1));
-				}
-			}
-			else
-			{
-				if (activator != nullptr && activator->IsKindOf (NAME_ScriptedMarine))
-				{
-					SetMarineWeapon(activator, STACK(1));
-				}
-			}
+			ScriptUtil::Exec(NAME_SetMarineWeapon, ScriptUtil::Pointer, activator, ScriptUtil::Int, STACK(2), ScriptUtil::Int, STACK(1), ScriptUtil::End);
 			sp -= 2;
 			break;
 
 		case PCD_SETMARINESPRITE:
-			{
-				PClassActor *type = PClass::FindActor(FBehavior::StaticLookupString (STACK(1)));
-
-				if (type != NULL)
-				{
-					if (STACK(2) != 0)
-					{
-						AActor *marine;
-						NActorIterator iterator(NAME_ScriptedMarine, STACK(2));
-
-						while ((marine = iterator.Next()) != NULL)
-						{
-							SetMarineSprite(marine, type);
-						}
-					}
-					else
-					{
-						if (activator != nullptr && activator->IsKindOf(NAME_ScriptedMarine))
-						{
-							SetMarineSprite(activator, type);
-						}
-					}
-				}
-				else
-				{
-					Printf ("Unknown actor type: %s\n", FBehavior::StaticLookupString (STACK(1)));
-				}
-			}
+			ScriptUtil::Exec(NAME_SetMarineSprite, ScriptUtil::Pointer, activator, ScriptUtil::Int, STACK(2), ScriptUtil::ACSClass, STACK(1), ScriptUtil::End);
 			sp -= 2;
 			break;
 
diff --git a/src/p_acs.h b/src/p_acs.h
index fc361a230..9115a370b 100644
--- a/src/p_acs.h
+++ b/src/p_acs.h
@@ -43,6 +43,7 @@
 class FFont;
 class FileReader;
 struct line_t;
+class FSerializer;
 
 
 enum
diff --git a/src/scripting/vm/vm.h b/src/scripting/vm/vm.h
index 1e5b65b2e..d52dc0f6f 100644
--- a/src/scripting/vm/vm.h
+++ b/src/scripting/vm/vm.h
@@ -675,6 +675,23 @@ VMFunction *FindVMFunction(PClass *cls, const char *name);
 
 FString FStringFormat(VM_ARGS, int offset = 0);
 
+#define IFVM(cls, funcname) \
+	static VMFunction * func = nullptr; \
+	if (func == nullptr) { \
+		func = dyn_cast<PFunction>(RUNTIME_CLASS(cls)->FindSymbol(#funcname, false)); \
+		assert(func); \
+	} \
+	if (func != nullptr)
+
+#define IFVMNAME(cls, funcname) \
+	static VMFunction * func = nullptr; \
+	if (func == nullptr) { \
+		func = dyn_cast<PFunction>(PClass::FindClass(cls)->FindSymbol(#funcname, false)); \
+		assert(func); \
+	} \
+	if (func != nullptr)
+
+
 
 unsigned GetVirtualIndex(PClass *cls, const char *funcname);
 
diff --git a/src/scriptutil.cpp b/src/scriptutil.cpp
new file mode 100644
index 000000000..f5d88087b
--- /dev/null
+++ b/src/scriptutil.cpp
@@ -0,0 +1,104 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright 2018 Christoph Oelckers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see http://www.gnu.org/licenses/
+//
+//-----------------------------------------------------------------------------
+//
+//
+// DESCRIPTION: generalized interface for implementing ACS/FS functions
+// in ZScript.
+//
+//-----------------------------------------------------------------------------
+
+#include "i_system.h"
+#include "tarray.h"
+#include "dobject.h"
+#include "vm.h"
+#include "scriptutil.h"
+#include "p_acs.h"
+
+
+static TArray<VMValue> parameters;
+static TMap<FName, VMFunction*> functions;
+
+
+void ScriptUtil::BuildParameters(va_list ap)
+{
+	for(int type = va_arg(ap, int); type != End; type = va_arg(ap, int))
+	{
+		switch (type)
+		{
+			case Int:
+				parameters.Push(VMValue(va_arg(ap, int)));
+				break;
+				
+			case Pointer:
+			case Class:		// this is just a pointer.
+			case String:	// must be passed by reference to a persistent location!
+				parameters.Push(VMValue(va_arg(ap, void*)));
+				break;
+				
+			case Float:
+				parameters.Push(VMValue(va_arg(ap, double)));
+				break;
+				
+			case ACSClass:
+				parameters.Push(VMValue(PClass::FindActor(FBehavior::StaticLookupString(va_arg(ap, int)))));
+				break;
+		}
+	}
+}
+
+void ScriptUtil::RunFunction(FName functionname, unsigned paramstart, VMReturn &returns)
+{
+	VMFunction *func = nullptr;
+	auto check = functions.CheckKey(functionname);
+	if (!check)
+	{
+		PClass::FindFunction(&func, NAME_ScriptUtil, functionname);
+		if (func == nullptr) 
+		{
+			I_Error("Call to undefined function ScriptUtil.%s", functionname.GetChars());
+		}
+		functions.Insert(functionname, func);
+	}
+	else func = *check;
+
+	VMCall(func, &parameters[paramstart], parameters.Size() - paramstart, &returns, 1);
+}
+
+int ScriptUtil::Exec(FName functionname, ...)
+{
+	unsigned paramstart = parameters.Size();
+	va_list ap;
+	va_start(ap, functionname);
+	try
+	{
+		BuildParameters(ap);
+		int ret = 0;
+		VMReturn returns(&ret);
+		RunFunction(functionname, paramstart, returns);
+		va_end(ap);
+		parameters.Clamp(paramstart);
+		return ret;
+	}
+	catch(...)
+	{
+		va_end(ap);
+		parameters.Clamp(paramstart);
+		throw;
+	}
+}
diff --git a/src/scriptutil.h b/src/scriptutil.h
new file mode 100644
index 000000000..6d8f83cc8
--- /dev/null
+++ b/src/scriptutil.h
@@ -0,0 +1,27 @@
+#pragma once
+
+
+#include <stdarg.h>
+#include "name.h"
+
+
+class ScriptUtil
+{
+	static void BuildParameters(va_list ap);
+	static void RunFunction(FName function, unsigned paramstart, VMReturn &returns);
+
+public:	
+	enum
+	{
+		End,
+		Int,
+		Pointer,
+		Float,
+		String,
+		Class,
+		ACSString,	// convenience helpers taking an ACS string index instead of a string
+		ACSClass,
+	};
+
+	static int Exec(FName functionname, ...);
+};
diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt
index 41b385141..0cc9c8f6b 100644
--- a/wadsrc/static/zscript.txt
+++ b/wadsrc/static/zscript.txt
@@ -260,3 +260,5 @@ version "3.7"
 #include "zscript/chex/chexitems.txt"
 #include "zscript/chex/chexdecorations.txt"
 #include "zscript/chex/chexplayer.txt"
+
+#include "zscript/scriptutil/scriptutil.txt"
diff --git a/wadsrc/static/zscript/scriptutil/scriptutil.txt b/wadsrc/static/zscript/scriptutil/scriptutil.txt
new file mode 100644
index 000000000..a47f79486
--- /dev/null
+++ b/wadsrc/static/zscript/scriptutil/scriptutil.txt
@@ -0,0 +1,105 @@
+
+// Container for utility functions used by ACS and FraggleScript.
+
+class ScriptUtil play
+{
+
+	//==========================================================================
+	//
+	//
+	//
+	//==========================================================================
+
+	static int SetWeapon(Actor activator, class<Inventory> cls)
+	{
+		if(activator != NULL && activator.player != NULL && cls != null)
+		{
+			let item = Weapon(activator.FindInventory(cls));
+
+			if(item != NULL)
+			{
+				if(activator.player.ReadyWeapon == item)
+				{
+					// The weapon is already selected, so setweapon succeeds by default,
+					// but make sure the player isn't switching away from it.
+					activator.player.PendingWeapon = WP_NOCHANGE;
+					return 1;
+				}
+				else
+				{
+					if(item.CheckAmmo(Weapon.EitherFire, false))
+					{
+						// There's enough ammo, so switch to it.
+						activator.player.PendingWeapon = item;
+						return 1;
+					}
+				}
+			}
+		}
+		return 0;
+	}
+
+	//==========================================================================
+	//
+	//
+	//
+	//==========================================================================
+
+	static void SetMarineWeapon(Actor activator, int tid, int marineweapontype)
+	{
+		if (tid != 0)
+		{
+			let it = ActorIterator.Create(tid, 'ScriptedMarine');
+			ScriptedMarine marine;
+
+			while ((marine = ScriptedMarine(it.Next())) != NULL)
+			{
+				marine.SetWeapon(marineweapontype);
+			}
+		}
+		else
+		{
+			let marine = ScriptedMarine(activator);
+			if (marine != null)
+			{
+				marine.SetWeapon(marineweapontype);
+			}
+		}
+	}
+
+	//==========================================================================
+	//
+	//
+	//
+	//==========================================================================
+
+	static void SetMarineSprite(Actor activator, int tid, class<Actor> type)
+	{
+		if (type != NULL)
+		{
+			if (tid != 0)
+			{
+				let it = ActorIterator.Create(tid, 'ScriptedMarine');
+				ScriptedMarine marine;
+
+				while ((marine = ScriptedMarine(it.Next())) != NULL)
+				{
+					marine.SetSprite(type);
+				}
+			}
+			else
+			{
+				let marine = ScriptedMarine(activator);
+				if (marine != null)
+				{
+					marine.SetSprite(type);
+				}
+			}
+		}
+		else
+		{
+			Console.Printf ("Unknown actor type: %s\n", type.GetClassName());
+		}
+	}
+	
+}