diff --git a/build/android/jni/Android.mk b/build/android/jni/Android.mk
index e58cb82c8a611d2581b3991a30f5f8ba9839dc8f..17c6fa7dd013e984ea3d4af8d6c51b294eed2a47 100644
--- a/build/android/jni/Android.mk
+++ b/build/android/jni/Android.mk
@@ -219,6 +219,7 @@ LOCAL_SRC_FILES :=                                \
 		jni/src/unittest/test_inventory.cpp       \
 		jni/src/unittest/test_mapnode.cpp         \
 		jni/src/unittest/test_nodedef.cpp         \
+		jni/src/unittest/test_noderesolver.cpp    \
 		jni/src/unittest/test_noise.cpp           \
 		jni/src/unittest/test_objdef.cpp          \
 		jni/src/unittest/test_profiler.cpp        \
diff --git a/src/nodedef.cpp b/src/nodedef.cpp
index 58566200e0dedf84dd7e4d141c9b7ffc19c76707..48207da1f98edda747ef33e8a9642e9276bad9b2 100644
--- a/src/nodedef.cpp
+++ b/src/nodedef.cpp
@@ -1346,7 +1346,8 @@ void CNodeDefManager::runNodeResolveCallbacks()
 //// NodeResolver
 ////
 
-NodeResolver::NodeResolver() {
+NodeResolver::NodeResolver()
+{
 	m_ndef            = NULL;
 	m_nodenames_idx   = 0;
 	m_nnlistsizes_idx = 0;
@@ -1379,14 +1380,14 @@ void NodeResolver::nodeResolveInternal()
 
 const std::string &NodeResolver::getNodeName(content_t c) const
 {
-	if (m_nodenames.size() == 0) {
+	if (c < m_nodenames.size())
+		return m_nodenames[c];
+
+	if (m_ndef)
 		return m_ndef->get(c).name;
-	} else {
-		if (c < m_nodenames.size())
-			return m_nodenames[c];
-		else
-			return m_ndef->get(CONTENT_UNKNOWN).name;
-	}
+
+	static const std::string unknown_str("unknown");
+	return unknown_str;
 }
 
 
@@ -1395,7 +1396,7 @@ bool NodeResolver::getIdFromNrBacklog(content_t *result_out,
 {
 	if (m_nodenames_idx == m_nodenames.size()) {
 		*result_out = c_fallback;
-		errorstream << "Resolver: no more nodes in list" << std::endl;
+		errorstream << "NodeResolver: no more nodes in list" << std::endl;
 		return false;
 	}
 
diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt
index ccb046bb95ab75f0d48f75a3d71c80daead1bf39..6ec5eaea514431412b54a938584828f04ca67fbe 100644
--- a/src/unittest/CMakeLists.txt
+++ b/src/unittest/CMakeLists.txt
@@ -7,6 +7,7 @@ set (UNITTEST_SRCS
 	${CMAKE_CURRENT_SOURCE_DIR}/test_inventory.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/test_mapnode.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/test_nodedef.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/test_noderesolver.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/test_noise.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/test_objdef.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/test_profiler.cpp
diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp
index 6fea0b8a02c1936b12bc9167a3efa96bf18adfa3..57843da5ee8f7c5ec27aa078ac70af6b905304f8 100644
--- a/src/unittest/test.cpp
+++ b/src/unittest/test.cpp
@@ -25,9 +25,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "itemdef.h"
 #include "gamedef.h"
 
-content_t CONTENT_STONE;
-content_t CONTENT_GRASS;
-content_t CONTENT_TORCH;
+content_t t_CONTENT_STONE;
+content_t t_CONTENT_GRASS;
+content_t t_CONTENT_TORCH;
+content_t t_CONTENT_WATER;
+content_t t_CONTENT_LAVA;
+content_t t_CONTENT_BRICK;
 
 ////////////////////////////////////////////////////////////////////////////////
 
@@ -111,7 +114,7 @@ void TestGameDef::defineSomeNodes()
 		f.tiledef[i].name = "default_stone.png";
 	f.is_ground_content = true;
 	idef->registerItem(itemdef);
-	CONTENT_STONE = ndef->set(f.name, f);
+	t_CONTENT_STONE = ndef->set(f.name, f);
 
 	//// Grass
 	itemdef = ItemDefinition();
@@ -131,7 +134,7 @@ void TestGameDef::defineSomeNodes()
 		f.tiledef[i].name = "default_dirt.png^default_grass_side.png";
 	f.is_ground_content = true;
 	idef->registerItem(itemdef);
-	CONTENT_GRASS = ndef->set(f.name, f);
+	t_CONTENT_GRASS = ndef->set(f.name, f);
 
 	//// Torch (minimal definition for lighting tests)
 	itemdef = ItemDefinition();
@@ -144,7 +147,69 @@ void TestGameDef::defineSomeNodes()
 	f.sunlight_propagates = true;
 	f.light_source = LIGHT_MAX-1;
 	idef->registerItem(itemdef);
-	CONTENT_TORCH = ndef->set(f.name, f);
+	t_CONTENT_TORCH = ndef->set(f.name, f);
+
+	//// Water
+	itemdef = ItemDefinition();
+	itemdef.type = ITEM_NODE;
+	itemdef.name = "default:water";
+	itemdef.description = "Water";
+	itemdef.inventory_image = "[inventorycube"
+		"{default_water.png"
+		"{default_water.png"
+		"{default_water.png";
+	f = ContentFeatures();
+	f.name = itemdef.name;
+	f.alpha = 128;
+	f.liquid_type = LIQUID_SOURCE;
+	f.liquid_viscosity = 4;
+	f.is_ground_content = true;
+	f.groups["liquids"] = 3;
+	for(int i = 0; i < 6; i++)
+		f.tiledef[i].name = "default_water.png";
+	idef->registerItem(itemdef);
+	t_CONTENT_WATER = ndef->set(f.name, f);
+
+	//// Lava
+	itemdef = ItemDefinition();
+	itemdef.type = ITEM_NODE;
+	itemdef.name = "default:lava";
+	itemdef.description = "Lava";
+	itemdef.inventory_image = "[inventorycube"
+		"{default_lava.png"
+		"{default_lava.png"
+		"{default_lava.png";
+	f = ContentFeatures();
+	f.name = itemdef.name;
+	f.alpha = 128;
+	f.liquid_type = LIQUID_SOURCE;
+	f.liquid_viscosity = 7;
+	f.light_source = LIGHT_MAX-1;
+	f.is_ground_content = true;
+	f.groups["liquids"] = 3;
+	for(int i = 0; i < 6; i++)
+		f.tiledef[i].name = "default_lava.png";
+	idef->registerItem(itemdef);
+	t_CONTENT_LAVA = ndef->set(f.name, f);
+
+
+	//// Brick
+	itemdef = ItemDefinition();
+	itemdef.type = ITEM_NODE;
+	itemdef.name = "default:brick";
+	itemdef.description = "Brick";
+	itemdef.groups["cracky"] = 3;
+	itemdef.inventory_image = "[inventorycube"
+		"{default_brick.png"
+		"{default_brick.png"
+		"{default_brick.png";
+	f = ContentFeatures();
+	f.name = itemdef.name;
+	for(int i = 0; i < 6; i++)
+		f.tiledef[i].name = "default_brick.png";
+	f.is_ground_content = true;
+	idef->registerItem(itemdef);
+	t_CONTENT_BRICK = ndef->set(f.name, f);
 }
 
 ////
diff --git a/src/unittest/test.h b/src/unittest/test.h
index c9a5a2e9ae4f08908933ac7d6a4a0d7461a59c1c..8219e30fc0f2eb64c464d560abed1f4816116bf4 100644
--- a/src/unittest/test.h
+++ b/src/unittest/test.h
@@ -124,9 +124,12 @@ class TestManager {
 };
 
 // A few item and node definitions for those tests that need them
-extern content_t CONTENT_STONE;
-extern content_t CONTENT_GRASS;
-extern content_t CONTENT_TORCH;
+extern content_t t_CONTENT_STONE;
+extern content_t t_CONTENT_GRASS;
+extern content_t t_CONTENT_TORCH;
+extern content_t t_CONTENT_WATER;
+extern content_t t_CONTENT_LAVA;
+extern content_t t_CONTENT_BRICK;
 
 void run_tests();
 
diff --git a/src/unittest/test_noderesolver.cpp b/src/unittest/test_noderesolver.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c440b2399c352381ed7150782836ff0b8430df4d
--- /dev/null
+++ b/src/unittest/test_noderesolver.cpp
@@ -0,0 +1,253 @@
+/*
+Minetest
+Copyright (C) 2010-2014 kwolekr, Ryan Kwolek <kwolekr@minetest.net>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "test.h"
+
+#include "exceptions.h"
+#include "gamedef.h"
+#include "nodedef.h"
+
+
+class TestNodeResolver : public TestBase {
+public:
+	TestNodeResolver() { TestManager::registerTestModule(this); }
+	const char *getName() { return "TestNodeResolver"; }
+
+	void runTests(IGameDef *gamedef);
+
+	void testNodeResolving(INodeDefManager *ndef);
+	void testPendingResolveCancellation(INodeDefManager *ndef);
+	void testDirectResolveMethod(INodeDefManager *ndef);
+	void testNoneResolveMethod(INodeDefManager *ndef);
+};
+
+static TestNodeResolver g_test_instance;
+
+void TestNodeResolver::runTests(IGameDef *gamedef)
+{
+	IWritableNodeDefManager *parent_ndef;
+	INodeDefManager *ndef;
+
+	parent_ndef = (IWritableNodeDefManager *)gamedef->getNodeDefManager();
+
+	ndef = parent_ndef->clone();
+	TEST(testNodeResolving, ndef);
+	delete ndef;
+
+	ndef = parent_ndef->clone();
+	TEST(testPendingResolveCancellation, ndef);
+	delete ndef;
+
+	ndef = parent_ndef->clone();
+	TEST(testDirectResolveMethod, ndef);
+	delete ndef;
+
+	ndef = parent_ndef->clone();
+	TEST(testNoneResolveMethod, ndef);
+	delete ndef;
+}
+
+class Foobar : public NodeResolver {
+public:
+	void resolveNodeNames();
+
+	content_t test_nr_node1;
+	content_t test_nr_node2;
+	content_t test_nr_node3;
+	content_t test_nr_node4;
+	content_t test_nr_node5;
+	std::vector<content_t> test_nr_list;
+	std::vector<content_t> test_nr_list_group;
+	std::vector<content_t> test_nr_list_required;
+	std::vector<content_t> test_nr_list_empty;
+};
+
+class Foobaz : public NodeResolver {
+public:
+	void resolveNodeNames();
+
+	content_t test_content1;
+	content_t test_content2;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Foobar::resolveNodeNames()
+{
+	UASSERT(getIdFromNrBacklog(&test_nr_node1, "", CONTENT_IGNORE) == true);
+	UASSERT(getIdsFromNrBacklog(&test_nr_list) == true);
+	UASSERT(getIdsFromNrBacklog(&test_nr_list_group) == true);
+	UASSERT(getIdsFromNrBacklog(&test_nr_list_required,
+		true, CONTENT_AIR) == false);
+	UASSERT(getIdsFromNrBacklog(&test_nr_list_empty) == true);
+
+	UASSERT(getIdFromNrBacklog(&test_nr_node2, "", CONTENT_IGNORE) == true);
+	UASSERT(getIdFromNrBacklog(&test_nr_node3,
+		"default:brick", CONTENT_IGNORE) == true);
+	UASSERT(getIdFromNrBacklog(&test_nr_node4,
+		"default:gobbledygook", CONTENT_AIR) == false);
+	UASSERT(getIdFromNrBacklog(&test_nr_node5, "", CONTENT_IGNORE) == false);
+}
+
+
+void Foobaz::resolveNodeNames()
+{
+	UASSERT(getIdFromNrBacklog(&test_content1, "", CONTENT_IGNORE) == true);
+	UASSERT(getIdFromNrBacklog(&test_content2, "", CONTENT_IGNORE) == false);
+}
+
+
+void TestNodeResolver::testNodeResolving(INodeDefManager *ndef)
+{
+	Foobar foobar;
+	size_t i;
+
+	foobar.m_nodenames.push_back("default:torch");
+
+	foobar.m_nodenames.push_back("default:dirt_with_grass");
+	foobar.m_nodenames.push_back("default:water");
+	foobar.m_nodenames.push_back("default:abloobloobloo");
+	foobar.m_nodenames.push_back("default:stone");
+	foobar.m_nodenames.push_back("default:shmegoldorf");
+	foobar.m_nnlistsizes.push_back(5);
+
+	foobar.m_nodenames.push_back("group:liquids");
+	foobar.m_nnlistsizes.push_back(1);
+
+	foobar.m_nodenames.push_back("default:warf");
+	foobar.m_nodenames.push_back("default:stone");
+	foobar.m_nodenames.push_back("default:bloop");
+	foobar.m_nnlistsizes.push_back(3);
+
+	foobar.m_nnlistsizes.push_back(0);
+
+	foobar.m_nodenames.push_back("default:brick");
+	foobar.m_nodenames.push_back("default:desert_stone");
+	foobar.m_nodenames.push_back("default:shnitzle");
+
+	ndef->pendNodeResolve(&foobar, NODE_RESOLVE_DEFERRED);
+	UASSERT(foobar.m_ndef == ndef);
+
+	ndef->setNodeRegistrationStatus(true);
+	ndef->runNodeResolveCallbacks();
+
+	// Check that we read single nodes successfully
+	UASSERTEQ(content_t, foobar.test_nr_node1, t_CONTENT_TORCH);
+	UASSERTEQ(content_t, foobar.test_nr_node2, t_CONTENT_BRICK);
+	UASSERTEQ(content_t, foobar.test_nr_node3, t_CONTENT_BRICK);
+	UASSERTEQ(content_t, foobar.test_nr_node4, CONTENT_AIR);
+	UASSERTEQ(content_t, foobar.test_nr_node5, CONTENT_IGNORE);
+
+	// Check that we read all the regular list items
+	static const content_t expected_test_nr_list[] = {
+		t_CONTENT_GRASS,
+		t_CONTENT_WATER,
+		t_CONTENT_STONE,
+	};
+	UASSERTEQ(size_t, foobar.test_nr_list.size(), 3);
+	for (i = 0; i != foobar.test_nr_list.size(); i++)
+		UASSERTEQ(content_t, foobar.test_nr_list[i], expected_test_nr_list[i]);
+
+	// Check that we read all the list items that were from a group entry
+	static const content_t expected_test_nr_list_group[] = {
+		t_CONTENT_WATER,
+		t_CONTENT_LAVA,
+	};
+	UASSERTEQ(size_t, foobar.test_nr_list_group.size(), 2);
+	for (i = 0; i != foobar.test_nr_list_group.size(); i++) {
+		UASSERT(CONTAINS(foobar.test_nr_list_group,
+			expected_test_nr_list_group[i]));
+	}
+
+	// Check that we read all the items we're able to in a required list
+	static const content_t expected_test_nr_list_required[] = {
+		CONTENT_AIR,
+		t_CONTENT_STONE,
+		CONTENT_AIR,
+	};
+	UASSERTEQ(size_t, foobar.test_nr_list_required.size(), 3);
+	for (i = 0; i != foobar.test_nr_list_required.size(); i++)
+		UASSERTEQ(content_t, foobar.test_nr_list_required[i],
+			expected_test_nr_list_required[i]);
+
+	// Check that the edge case of 0 is successful
+	UASSERTEQ(size_t, foobar.test_nr_list_empty.size(), 0);
+}
+
+
+void TestNodeResolver::testPendingResolveCancellation(INodeDefManager *ndef)
+{
+	Foobaz foobaz1;
+	foobaz1.test_content1 = 1234;
+	foobaz1.test_content2 = 5678;
+	foobaz1.m_nodenames.push_back("default:dirt_with_grass");
+	foobaz1.m_nodenames.push_back("default:abloobloobloo");
+	ndef->pendNodeResolve(&foobaz1, NODE_RESOLVE_DEFERRED);
+
+	Foobaz foobaz2;
+	foobaz2.test_content1 = 1234;
+	foobaz2.test_content2 = 5678;
+	foobaz2.m_nodenames.push_back("default:dirt_with_grass");
+	foobaz2.m_nodenames.push_back("default:abloobloobloo");
+	ndef->pendNodeResolve(&foobaz2, NODE_RESOLVE_DEFERRED);
+
+	ndef->cancelNodeResolveCallback(&foobaz1);
+
+	ndef->setNodeRegistrationStatus(true);
+	ndef->runNodeResolveCallbacks();
+
+	UASSERT(foobaz1.test_content1 == 1234);
+	UASSERT(foobaz1.test_content2 == 5678);
+	UASSERT(foobaz2.test_content1 == t_CONTENT_GRASS);
+	UASSERT(foobaz2.test_content2 == CONTENT_IGNORE);
+}
+
+
+void TestNodeResolver::testDirectResolveMethod(INodeDefManager *ndef)
+{
+	Foobaz foobaz;
+
+	foobaz.m_nodenames.push_back("default:dirt_with_grass");
+	foobaz.m_nodenames.push_back("default:abloobloobloo");
+
+	UASSERTEQ(std::string, foobaz.getNodeName(1), "default:abloobloobloo");
+
+	ndef->pendNodeResolve(&foobaz, NODE_RESOLVE_DIRECT);
+
+	UASSERTEQ(content_t, foobaz.test_content1, t_CONTENT_GRASS);
+	UASSERTEQ(content_t, foobaz.test_content2, CONTENT_IGNORE);
+
+	// We expect this to be *different* because the resolution of this node had
+	// failed.  The internal nodename buffer is cleared and lookups should now
+	// use the nodedef manager.
+	UASSERT(foobaz.getNodeName(1) != "default:abloobloobloo");
+}
+
+
+void TestNodeResolver::testNoneResolveMethod(INodeDefManager *ndef)
+{
+	Foobaz foobaz;
+
+	foobaz.m_nodenames.push_back("default:dirt_with_grass");
+	foobaz.m_nodenames.push_back("default:abloobloobloo");
+
+	ndef->pendNodeResolve(&foobaz, NODE_RESOLVE_NONE);
+
+	UASSERTEQ(std::string, foobaz.getNodeName(1), "default:abloobloobloo");
+}
diff --git a/src/unittest/test_voxelalgorithms.cpp b/src/unittest/test_voxelalgorithms.cpp
index 2093ff5f8fee7dcc315435d292bbb72abb7c5433..31b9cadd56690f83e45e7a2e913d869759745a77 100644
--- a/src/unittest/test_voxelalgorithms.cpp
+++ b/src/unittest/test_voxelalgorithms.cpp
@@ -69,7 +69,7 @@ void TestVoxelAlgorithms::testPropogateSunlight(INodeDefManager *ndef)
 				== LIGHT_SUN);
 	}
 
-	v.setNodeNoRef(v3s16(0,0,0), MapNode(CONTENT_STONE));
+	v.setNodeNoRef(v3s16(0,0,0), MapNode(t_CONTENT_STONE));
 
 	{
 		std::set<v3s16> light_sources;
@@ -91,7 +91,7 @@ void TestVoxelAlgorithms::testPropogateSunlight(INodeDefManager *ndef)
 				== 0);
 	}
 
-	v.setNodeNoRef(v3s16(1,3,2), MapNode(CONTENT_STONE));
+	v.setNodeNoRef(v3s16(1,3,2), MapNode(t_CONTENT_STONE));
 
 	{
 		std::set<v3s16> light_sources;
@@ -180,8 +180,8 @@ void TestVoxelAlgorithms::testClearLightAndCollectSources(INodeDefManager *ndef)
 	}
 
 	VoxelArea a(v3s16(0,0,0), v3s16(2,2,2));
-	v.setNodeNoRef(v3s16(0,0,0), MapNode(CONTENT_STONE));
-	v.setNodeNoRef(v3s16(1,1,1), MapNode(CONTENT_TORCH));
+	v.setNodeNoRef(v3s16(0,0,0), MapNode(t_CONTENT_STONE));
+	v.setNodeNoRef(v3s16(1,1,1), MapNode(t_CONTENT_TORCH));
 
 	{
 		MapNode n(CONTENT_AIR);
diff --git a/src/unittest/test_voxelmanipulator.cpp b/src/unittest/test_voxelmanipulator.cpp
index d219e62e03d41807d9daac092ddca055135338be..7f8ba3849f0a3d18cc6b174a48a9d2d5eba0555b 100644
--- a/src/unittest/test_voxelmanipulator.cpp
+++ b/src/unittest/test_voxelmanipulator.cpp
@@ -87,10 +87,10 @@ void TestVoxelManipulator::testVoxelManipulator(INodeDefManager *nodedef)
 	v.print(infostream, nodedef);
 
 	infostream << "*** Setting (-1,0,-1)=2 ***" << std::endl;
-	v.setNodeNoRef(v3s16(-1,0,-1), MapNode(CONTENT_GRASS));
+	v.setNodeNoRef(v3s16(-1,0,-1), MapNode(t_CONTENT_GRASS));
 
 	v.print(infostream, nodedef);
-	UASSERT(v.getNode(v3s16(-1,0,-1)).getContent() == CONTENT_GRASS);
+	UASSERT(v.getNode(v3s16(-1,0,-1)).getContent() == t_CONTENT_GRASS);
 
 	infostream << "*** Reading from inexistent (0,0,-1) ***" << std::endl;
 
@@ -103,6 +103,6 @@ void TestVoxelManipulator::testVoxelManipulator(INodeDefManager *nodedef)
 	v.addArea(a);
 	v.print(infostream, nodedef);
 
-	UASSERT(v.getNode(v3s16(-1,0,-1)).getContent() == CONTENT_GRASS);
+	UASSERT(v.getNode(v3s16(-1,0,-1)).getContent() == t_CONTENT_GRASS);
 	EXCEPTION_CHECK(InvalidPositionException, v.getNode(v3s16(0,1,1)));
 }