[UMod] Editor+ (minor editor improvements)

Ask for help about creating mods and scripts for Grimrock 2 or share your tips, scripts, tools and assets with other modders here. Warning: forum contains spoilers!
Post Reply
User avatar
7Soul
Posts: 199
Joined: Sun Oct 19, 2014 1:56 am
Location: Brazil

[UMod] Editor+ (minor editor improvements)

Post by 7Soul »

This mod adds a few minor changes to the editor:

- Displays map number in map list
- Ability to use up/down arrows to reorder maps
- Ability to filter objects by their traits
- Key shortcuts to restart the editor (Ctrl+Y) and go back to menu (Ctrl+T)
- Makes map list a little bigger by default

Image


How to use:

1 - First you have to be on the new beta branch. On steam, right click the game > Properties > Betas. Add the code "ggllooeegggg" to unlock the secret "nutcracker" beta

2 - Go to "\Documents\Almost Human\legend of grimrock 2". Once the beta is downloaded, you'll see a file named "mods.cfg" and a "Mods" folder

3 - In the Mods folder, create a text file and paste the code from the end of this post, name it "editorPlus.lua" (or any name you want). Confirm when windows ask if you want to change the extension

4 - Add the mod to mods.cfg so it looks like this:

Code: Select all

mods = {
	"editorPlus.lua",
}

Code:

Code: Select all

-- This file contains sections of Legend of Grimrock 2 source code; anything you
-- do with this file must comply with the Grimrock modding terms:
-- http://www.grimrock.net/modding_log1/modding-and-asset-usage-terms/
--
-- You are free to alter this mod or reuse its code in other Grimrock mods.

--[=[
=== UModManager Info Section ===

id = "EditorPlus"

name = "Editor Plus"

description = [[Adds some minor improvements to the dungeon editor.

Made by 7Soul (henriquelazarini@gmail.com)]]

version = "0.2.0"

priority = 100

modifiedFields = { "DungeonEditor.init" }

overwrittenFields = { "DungeonEditor.update", "DungeonEditor.projectExplorer", "DungeonEditor.assetBrowser", "DungeonEditor.drawConnectors" }

=== End of Mod Info ===
]=]

DungeonEditor.backToMenuKey = "T" -- ctrl + this key goes back to main menu
DungeonEditor.fullRestart = "Y"   -- ctrl + this key restarts the editor and reloads mods

-------------------------------------------------------------------------------------------------------
-- Editor Functions                                                                                  --    
-------------------------------------------------------------------------------------------------------

local oldDungeonInit = DungeonEditor.init
function DungeonEditor:init()
	oldDungeonInit(self)
	self.splitter1 = 252
	self.splitter2 = 476
	self.splitter3 = 340
	self.splitter4 = 200 + 310 -- larger map list
end

function DungeonEditor:update()
	if not renderer:isReadyToRender() or (config.sleepWhenNoFocus and not mainFrame:hasFocus()) then
		sys.sleep(100)
		return
	end

	if self.pendingLoadDungeon then
		self:loadDungeon(self.pendingLoadDungeon)
		self.pendingLoadDungeon = nil
	end

	updateTime()
	updateFileChangeRequests()
	
	--sys.sleep(50)
	
	-- update input state
	local windowSizeChanged = false
	do
		local state = imgui.state
		state.doubleClick = detectDoubleClick()

		-- clear unprocessed keys
		state.keyInput = {}
		state.mouseWheel = 0
		
		-- poll events
		while true do
			local event = mainFrame:pollEvents()
			if not event then break end
			if event.type == "key" and event.down then
				--print(event.key, event.char)			
				state.keyInput[#state.keyInput+1] = event
				
				-- global keys
				local action = config:convertEditorKeyToAction(event.keyCode, event.modifiers)
				if event.key == "O" and event.modifiers == 2 then self:onOpenProject() end
				if event.key == "S" and event.modifiers == 2 then self:onSaveProject() end
				if event.key == "R" and event.modifiers == 2 then self:onReloadProject() end
				if event.key == DungeonEditor.backToMenuKey and event.modifiers == 2 then self:onBackToGame() end
				if event.key == DungeonEditor.fullRestart and event.modifiers == 2 then sys.restart{ "launchEditor" } end
				if action == "start_preview" then self:playPreview() end
				if action == "stop_preview" then self:stopPreview() end
			elseif event.type == "mouse_wheel" then
				state.mouseWheel = state.mouseWheel + event.delta
			elseif event.type == "menu" then
				self:onMenuEvent(event.id)
			elseif event.type == "resize" then
				self.windowWidth = event.width	-- 0 when window is minimized
				self.windowHeight = event.height
				windowSizeChanged = true
			elseif event.type == "close" then
				self:confirmClose(function() sys.exit() end)
			end
		end		
	end

	-- early out if window is minimized
	if self.windowWidth == 0 or self.windowHeight == 0 then return end

	-- recreate render window if window was resized
	if windowSizeChanged then
		renderer:resizeRenderBuffers(self.windowWidth, self.windowHeight)
	end
		
	-- update steam
	steamContext:update()
		
	renderer:setViewport(0, 0, self.windowWidth, self.windowHeight)
	renderer:beginRender()
	
	ImmediateMode.beginDraw()
	imgui.prepare(mainFrame)

	local screenWidth = self.windowWidth
	local screenHeight = self.windowHeight

	if self.previewMode and self.fullscreen then
		-- fullscreen preview mode
		self:preview(0, 0, screenWidth, screenHeight)
	else
		ImmediateMode.fillRect(0, 0, screenWidth, screenHeight, {41,41,41,255})
		
		self:toolBar(20, 10, 200, 30)
		self:brushInfo(self.splitter1 + 2, 10, (screenWidth - self.splitter1) - self.splitter2 - 4, 30)

		-- screen height without status bar
		local screenHeight2 = screenHeight - 20
		
		-- project/asset browser splitter
		do
			local x = 0
			local y = 44
			local width = self.splitter1 - 2
			local height = screenHeight2 - y
			
			-- asset browser
			do
				local y = self.splitter4+2
				local height = screenHeight2 - y
				self:assetBrowser(x, y, width, height)
			end
			
			-- project explorer
			do
				local height = self.splitter4 - y - 2
				self:projectExplorer(x, y, width, height)
			end

			-- splitter 4
			self.splitter4 = imgui.vsplitter("splitter4", x, self.splitter4, width)
			self.splitter4 = math.clamp(self.splitter4, y + 50, screenHeight2 - 22)
		end
		
		-- map view
		do
			local x = self.splitter1 + 4
			local y = 44
			local width = (screenWidth - self.splitter2) - self.splitter1 - 8
			local height = screenHeight2 - y
			self:mapView(x, y, width, height)
		end
		
		-- preview/inspector splitter
		do
			local x = (screenWidth - self.splitter2) + 2
			local y = 44
			local width = screenWidth - x
			local height = screenHeight2 - y
			
			-- inspectors
			do
				local y = self.splitter3+2
				local height = screenHeight2 - y
				local sel = iff(#self.selection == 1, self.selection[1], nil)							
				self.inspector:inspect(sel, x, y, width, height, 0)
			end
				
			-- preview
			do
				local height = self.splitter3 - y - 2
				self:previewButtons(x, 10)
				if self.previewMode and self.fullscreen then
					self:preview(0, 0, screenWidth, screenHeight)
				else
					self:preview(x, y, width, height)
				end
			end

			-- splitter 3
			self.splitter3 = imgui.vsplitter("splitter3", x, self.splitter3, width)
			self.splitter3 = math.clamp(self.splitter3, y + 50, screenHeight2 - 22)
		end
				
		-- status bar
		do
			local h = 17
			local x = 0
			local y = screenHeight - h
			ImmediateMode.fillRect(x, y, screenWidth, h, {56,56,56,255})
			if self.status then ImmediateMode.drawText(self.status, x+4, y+2, imgui.state.font, {200,200,200,255}) end
			
			-- draw coordinates
			if self.mouseCellX and self.mouseCellY then
				local text = string.format("%d,%d", self.mouseCellX, self.mouseCellY)
				local textWidth = imgui.state.font:getTextWidth(text)
				local x = x + screenWidth - textWidth - 4
				ImmediateMode.fillRect(x-4, y, textWidth+8, h, {56,56,56,255})
				ImmediateMode.drawText(text, x, y + 2, imgui.state.font, {200,200,200,255})
			end
		end
		
		-- splitter 1 (measured from left screen edge)
		self.splitter1 = imgui.hsplitter("splitter1", self.splitter1, 0, screenHeight)
		self.splitter1 = math.clamp(self.splitter1, 10, (screenWidth - self.splitter2)-10)
		
		-- splitter 2 (measured from right screen edge)
		self.splitter2 = screenWidth - imgui.hsplitter("splitter2", screenWidth - self.splitter2, 0, screenHeight)
		self.splitter2 = math.clamp(self.splitter2, 10, (screenWidth - self.splitter1)-10)

		self:updateContextMenu()
	end
	
	if self.dialog then
		self.dialog:update()
		if self.dialog.close then self.dialog = nil end
	end

	imgui.finish()
	
	ImmediateMode.endDraw()

	renderer:endRender()

	tvec.free()
	tmat.free()
end

-- Project explorer

function DungeonEditor:projectExplorer(x, y, width, height)
	x,y,width,height = self:panel("Project", x, y, width, height)	

	if not dungeon then return end
	
	imgui.beginArea(x, y, width, height)

	ImmediateMode.drawRect(x, y, x+width-1, y+height-1, {50,50,50,255})
	
	local lineHeight = 13
	local scroll = self.projectScroll
	
	do
		local y = y
		for i=1,#dungeon.maps do
			local map = dungeon.maps[i]
			
			-- level visibility
			local oldState = map._levelDisabled
			if oldState == nil then oldState = false end
			map._levelDisabled = not self:levelTick("level_check_"..i, x, y - scroll, 16, lineHeight, not map._levelDisabled)
			
			-- special case: control click hides all other levels
			if oldState ~= map._levelDisabled and sys.keyDown("control") then
				for j=1,#dungeon.maps do
					local map = dungeon.maps[j]
					map._levelDisabled = (i ~= j)
				end
				self.map = dungeon.maps[i]
			end

			-- level coord and name
			local x = x + 20
			local lx,ly,lz = map:getLevelCoord()
			local item = string.format("%02d (%d,%d,%d) %s", i, lx, ly, lz, map.name) -- show map id
			if map == self.map then
				ImmediateMode.fillRect(x, y - scroll, width, lineHeight, {48,72,96,255})
				local color = iff(map._levelDisabled, {200,200,200,255}, Color.White)
				ImmediateMode.drawText(item, x, y - scroll, imgui.state.font, color)
			else
				local color = iff(map._levelDisabled, {115,115,115,255}, {206,206,206,255})
				ImmediateMode.drawText(item, x, y - scroll, imgui.state.font, color)
			end
			y = y + lineHeight
		end
	end
	
	imgui.endArea()

	if imgui.buttonLogic("project_explorer", x + 14, y, width - 14, height) then
		local y = math.floor((imgui.state.mouseY - y + scroll) / lineHeight) + 1
		if y >= 1 and y <= #dungeon.maps then
			self.map = dungeon.maps[y]
		end
	end
	
	-- context menu
	if imgui.state.hot == "project_explorer" and sys.mousePressed(2) then
		local state = imgui.state
		local y = math.floor((imgui.state.mouseY - y + scroll) / lineHeight) + 1
		if y >= 1 and y <= #dungeon.maps then
			self.map = dungeon.maps[y]
			self:contextMenu{
				"New Level", function() self:newLevel() end,
				"Delete", function() self:deleteLevel() end,
				"Move Up", function() self:moveLevelUp() end,
				"Move Down", function() self:moveLevelDown() end,
				"Sort", function() self:sortLevels() end,
				"Properties", function() self.dialog = MapPropertiesDialog.create() end,
			}
		end
	end

	if imgui.state.hot == "project_explorer" and not self.dialog then
		while #imgui.state.keyInput > 0 do
			local ev = imgui.state.keyInput[1]
			table.remove(imgui.state.keyInput, 1)
		
			local action = config:convertEditorKeyToAction(ev.keyCode, ev.modifiers)
			
			if ev.key == "up" and ev.modifiers == 0 then self:moveLevelUp() end
			if ev.key == "down" and ev.modifiers == 0 then self:moveLevelDown() end
		end
	end

	-- scroll bar
	local numItems = #dungeon.maps
	self.projectScroll = imgui.vscrollbar("project_scroller", x+width-10, y, 10, height, self.projectScroll, height, numItems*lineHeight)
	
	-- mouse wheel scrolling
	if imgui.state.hot == "project_explorer" and imgui.state.mouseWheel ~= 0 then
		self.projectScroll = self.projectScroll - imgui.state.mouseWheel * lineHeight * 5
	end

	self.projectScroll = math.clamp(self.projectScroll, 0, math.max(numItems*lineHeight - height, 0))	
end

function DungeonEditor:assetBrowser(x, y, width, height)
	--ImmediateMode.fillRect(x, y, width, height, {50,50,50,255})

	x,y,width,height = self:panel("Asset Browser", x, y, width, height)
	x = x + 2
	y = y + 0
	width = width - 2
	height = height

	if not dungeon then return end
	
	ImmediateMode.pushState()
	ImmediateMode.clipTo(x, y, x+width, y+height)	

	-- search field
	do
		local w = math.min(120, width)
		self.findAsset = self:searchBox("asset_find", x, y+1, width, nil, self.findAsset)
		y = y + 20
		height = height - 20
	end
	
	-- collect set of tags from archs
	local tags = {}
	do
		local s = {}
		for _,a in pairs(dungeon.archs) do
			if a.editorIcon then
				for t,_ in pairs(a.tags) do
					s[t] = true
				end
			end
		end

		-- convert to list of tags
		for t,_ in pairs(s) do
			tags[#tags+1] = t
		end
		table.sort(tags)
		table.insert(tags, 1, "any")
	end

	-- collect set of traits from archs
	local traits = {}
	do
		local s = {}
		for _,a in pairs(dungeon.archs) do
			if a.editorIcon and a.components then
				for _,c in ipairs(a.components) do
					if c.traits then
						for _,t in pairs(c.traits) do
							s[t] = true
						end
					end
				end
			end
		end

		-- convert to list of traits
		for t,_ in pairs(s) do
			traits[#traits+1] = t
		end
		table.sort(traits)
		table.insert(traits, 1, "any")
	end
	
	-- filter
	if self.mode ~= "brush_tool" then
		y = y + 3
		imgui.label("Tags", x+2, y+3)
		self.assetFilter = imgui.combobox("asset_filter", x+45, y, 135, 18, self.assetFilter or 1, tags)
		y = y + 23
		height = height - 26

		y = y + 3
		imgui.label("Traits", x+2, y+3)
		self.assetFilter2 = imgui.combobox("asset_filter2", x+50, y, 135, 18, self.assetFilter2 or 1, traits)
		y = y + 23
		height = height - 26
	else
		local h = 3
		y = y + h
		height = height - h
	end
	
	-- list of assets
	do
		local height = height
		
		ImmediateMode.pushState()
		ImmediateMode.clipTo(x, y, x+width, y+height)	

		ImmediateMode.drawRect(x, y, x+width-1, y+height-1, {50,50,50,255})
		
		local filter = tags[self.assetFilter]
		local filter2 = traits[self.assetFilter2]
		
		local assets = {}
		
		if self.mode == "brush_tool" then
			-- tiles
			for _,tile in pairs(self.dungeon.tiles) do
				if not self.currentBrush[1] then self.currentBrush[1] = tile end
				if not self.currentBrush[2] then self.currentBrush[2] = tile end

				-- filter asset by name
				local ignore
				if #self.findAsset > 0 and not string.match(tile.name, self.findAsset, 1, true) then
					ignore = true
				end
		
				if not ignore then assets[#assets+1] = tile end
			end
		else
			-- archs
			for _,a in pairs(dungeon.archs) do
				if a.editorIcon and not string.match(a.name, "^base%_") then
					-- filter by tags
					local ignore
					if filter ~= "any" and not a.tags[filter] then 
						ignore = true
					end

					-- filter by traits
					if a.components then
						local traitComp
						for _,c in ipairs(a.components) do
							if c.traits then
								traitComp = c
							end
						end

						if traitComp then
							if filter2 ~= "any" and not table.contains(traitComp.traits, filter2) then
								ignore = true
							end
						else
							if filter2 ~= "any" then
								ignore = true
							end
						end
					end
					
					if a.name == "party" then ignore = true end
					
					-- filter asset by name
					if #self.findAsset > 0 and not string.find(a.name, self.findAsset, 1, true) then
						ignore = true
					end
					
					if not ignore then
						assets[#assets+1] = a
					end
				end
			end
		end
		
		-- sort alphabetically
		table.sort(assets, function(l,r) return l.name < r.name end)

		local lineHeight = 20
		
		do
			local y = y
			for i=1,#assets do
				local a = assets[i]
				
				local selected
				if self.mode == "brush_tool" then
					selected = (self.currentBrush[1] == a)
				else
					selected = (self.selectedAsset == a)
				end
				
				if self.currentBrush[2] == a then
					ImmediateMode.fillRect(x, y - self.assetScroll, width, lineHeight, {48,72,96,128})
				end
				
				if selected then
					ImmediateMode.fillRect(x, y - self.assetScroll, width, lineHeight, {48,72,96,255})
					ImmediateMode.drawText(a.name, x+22, y - self.assetScroll+4, imgui.state.font, Color.White)
				else
					ImmediateMode.drawText(a.name, x+22, y - self.assetScroll+4, imgui.state.font, {200,200,200,255})
				end

				if a.editorIcon then
					local y = y - self.assetScroll
					if a.editorIcon == 24 then y = y + 6 end
					self:drawMapTile(x, y, a.editorIcon, false, 0, a.color or Color.White)
				end

				y = y + lineHeight
			end
		end
		
		ImmediateMode.popState()

		if self.mode == "brush_tool" then
			-- imgui.buttonLogic() does not work with rmb...
			local hover = imgui.regionHit(x, y, width, height)
			if hover then
				local y = math.floor((imgui.state.mouseY - y + self.assetScroll) / lineHeight) + 1
				
				if imgui.state.hot == "asset_browser" then
					if sys.mousePressed(0) then self.currentBrush[1] = assets[y] end
					if sys.mousePressed(2) then self.currentBrush[2] = assets[y] end
				end

				imgui.state.newHot = "asset_browser"
			end
		else
			if imgui.buttonLogic("asset_browser", x, y, width, height) then
				local y = math.floor((imgui.state.mouseY - y + self.assetScroll) / lineHeight) + 1
				self.selectedAsset = assets[y]
				self.mode = "place_objects"
				self.status = "Add Object: Click on the map to add object"				
				imgui.state.focus = "asset_browser"
			end
		end
		
		-- scroll bar
		self.assetScroll = imgui.vscrollbar("asset_scroller", x+width-10, y, 10, height, self.assetScroll, height, #assets*lineHeight)
		
		-- mouse wheel scrolling
		if imgui.state.hot == "asset_browser" and imgui.state.mouseWheel ~= 0 then
			self.assetScroll = self.assetScroll - imgui.state.mouseWheel * lineHeight * 4
		end

		self.assetScroll = math.clamp(self.assetScroll, 0, math.max(#assets*lineHeight - height, 0))
	end

	ImmediateMode.popState()
end

function DungeonEditor:drawConnectors(x, y, ent, color)
	local s = 20
	
	for i=1,ent.components.length do
		local comp = ent.components[i]

		if comp.connectors then
			for _,connector in ipairs(comp.connectors) do
				local target = connector.target
				if target then
					target = self.map:findEntity(target)
					if target then
						local x1 = x + ent.x * s + s/2
						local y1 = y + ent.y * s + s/2
						if ent.arch.placement == "wall" then
							local dx,dy = getDxDy(ent.facing)
							x1 = x1 + dx*10
							y1 = y1 + dy*10
						elseif ent.arch.placement == "pillar" then
							x1 = x1 - 10
							y1 = y1 - 10
						end

						local x2 = x + target.x * s + s/2
						local y2 = y + target.y * s + s/2
						if target.arch.placement == "wall" then
							local dx,dy = getDxDy(target.facing)
							x2 = x2 + dx*10
							y2 = y2 + dy*10
						elseif target.arch.placement == "pillar" then
							x2 = x2 - 10
							y2 = y2 - 10
						end

						self:drawConnectorArrow(x1, y1, x2, y2, color)
					end
				end
			end
		end
		
		if comp.__class == TeleporterComponent or comp.__class == StairsComponent then
			local tlevel,tx,ty = comp:getTeleportTarget(comp)
			if tx and ty then
				local x1 = x + ent.x * s + s/2
				local y1 = y + ent.y * s + s/2
				local x2 = x + tx * s + s/2
				local y2 = y + ty * s + s/2
				self:drawConnectorArrow(x1, y1, x2, y2, color)
			end
		end
	end
end
Last edited by 7Soul on Wed Jan 12, 2022 3:06 pm, edited 2 times in total.
Join the LoG discord server: https://discord.gg/ArgAgNN :D

My Mods
kelly1111
Posts: 349
Joined: Sun Jan 20, 2013 6:28 pm

Re: [UMod] Editor+ (minor editor improvements)

Post by kelly1111 »

Very nice. thank you.
kelly1111
Posts: 349
Joined: Sun Jan 20, 2013 6:28 pm

Re: [UMod] Editor+ (minor editor improvements)

Post by kelly1111 »

I have noticed something odd with the editor after installing the mod. When I want to delete an object with the del key that is on top of other objects (stacked), it wont let me delete it. It does however let me delete single objects.
User avatar
7Soul
Posts: 199
Joined: Sun Oct 19, 2014 1:56 am
Location: Brazil

Re: [UMod] Editor+ (minor editor improvements)

Post by 7Soul »

kelly1111 wrote: Fri Jan 22, 2021 11:24 am I have noticed something odd with the editor after installing the mod. When I want to delete an object with the del key that is on top of other objects (stacked), it wont let me delete it. It does however let me delete single objects.
Updated in the first post
Join the LoG discord server: https://discord.gg/ArgAgNN :D

My Mods
Killcannon
Posts: 73
Joined: Sun Apr 12, 2015 2:57 pm

Re: [UMod] Editor+ (minor editor improvements)

Post by Killcannon »

Question, how do we get the Beta on GoG, and other non-steam related platforms?
User avatar
7Soul
Posts: 199
Joined: Sun Oct 19, 2014 1:56 am
Location: Brazil

Re: [UMod] Editor+ (minor editor improvements)

Post by 7Soul »

Killcannon wrote: Wed Jun 30, 2021 1:12 am Question, how do we get the Beta on GoG, and other non-steam related platforms?
I don't think it's possible yet :/
Join the LoG discord server: https://discord.gg/ArgAgNN :D

My Mods
User avatar
7Soul
Posts: 199
Joined: Sun Oct 19, 2014 1:56 am
Location: Brazil

Re: [UMod] Editor+ (minor editor improvements)

Post by 7Soul »

Small update 0.2.0 (copy code in first post):

- Connector arrows now work with objects that use "pillar" placement
Image
Join the LoG discord server: https://discord.gg/ArgAgNN :D

My Mods
User avatar
Isaac
Posts: 3172
Joined: Fri Mar 02, 2012 10:02 pm

Re: [UMod] Editor+ (minor editor improvements)

Post by Isaac »

That's really cool. 8-)

I wish Petri had updated the GoG and Direct sales versions before abandoning the business. :(
(I do not have the Steam version, and it irks that anyone who would play a Umod has to have the Steam version to do it.)
Post Reply