-- for client : register a component into C using the 'registerGenerator' function and add -- it into the list of classes -- copy class properties & methods local reservedMembers = { Prop = true, BaseClass = true, NameToProp = true, -- native properties Parent = true, ParentInstance= true, IndexInParent = true, User = true, Size = true, DisplayerUI = true, DisplayerVisual = true, DisplayerProperties = true, Selectable = true, SelectableFromRoot = true } -- check is a string is a valid identifier (e.g begin with an alphabetic letter followed by alpha numeric letters) local function isValidIdentifier(id) return string.match(id, "[%a_][%w_]*") end -- register a new class (features, components ...) r2.registerComponent = function(generator, package) -- check that all identifiers are correct local badProps = {} if generator.Prop == nil then generator.Prop = {} end -- serach invalid identifiers for k, prop in pairs(generator.Prop) do if (type(prop) ~= "table") then debugWarning("Invalid property found in ".. generator.Name) end if not isValidIdentifier( prop.Name ) then debugWarning("Invalid property name found : '" .. tostring(prop.Name) .. "' in class " .. tostring(generator.Name) .. ". Property is ignored") table.insert(badProps, k) end if reservedMembers[prop.Name] then debugWarning("Invalid property name found : '" .. tostring(prop.Name) .. "' in class " .. tostring(generator.Name) .. ". This is the name of a native poperty, and can't be redefined") table.insert(badProps, k) end end -- check that no identifier is duplicated local propName = {} for k, prop in pairs(generator.Prop) do if propName[prop.Name] ~= nil then debugWarning("Duplicated property found when registering class " .. tostring(generator.Name) .. ", property = " .. tostring(prop.Name)) debugWarning("Aborting class registration.") return end propName[prop.Name] = true end -- remove bad properties from the class for k, prop in pairs(badProps) do generator.Prop[prop] = nil end local name = nil if package then name = generator.Name --name = package:getUniqId() .. "|" .. generator.Name TODO else name = generator.Name; end if r2.Classes[name] ~= nil then debugWarning("Component registered twice : " .. generator.Name) return end if type(name) ~= "string" then debugWarning("Can't register class, 'Name' field not found or bad type") assert(false) end r2.Classes[name] = generator end -- private : perform subclassing by copying properties / methods of the base class into the class passed as a parameter -- only methods / props that are present in the base class and not in the derived class are copied -- subclass is done recursively so after calling 'subclass' on a class definition it will be complete function r2.Subclass(classDef) assert(classDef) assert(type(classDef) == "table") if classDef.BaseClass ~= "" and classDef.BaseClass ~= nil then local baseClass = r2.Classes[classDef.BaseClass] if baseClass == nil then debugInfo("Cant found base class " .. strify(classDef.BaseClass) .. " of class " .. strify(classDef.Name)) return end --debugInfo("sub classing " .. tostring(classDef.Name) .. " from " .. tostring(baseClass.Name)) -- make sure that base class is complete, too r2.Subclass(baseClass) for key, value in pairs(baseClass) do if classDef[key] == nil then -- if property or method not defined in derived class then copy from the base class -- if this is a method, then just replace with a function that will delegate -- the call to the parent class --if type(value) == "function" and key ~= "delegate" then -- assert(type(key) == "string") -- local localKey = key -- apparently, closure are not supported with locals from the enclosing 'for' loop -- local function delegator(this, ...) --debugInfo("Calling parent version in parent class '" .. tostring(baseClass.Name) .. "' for " .. tostring(localKey)) -- TODO nico : here if would be much faster to call the -- first parent class where function is defined instead -- of chaining the calls to 'delegate' -- There are 1 thing to rememeber however : -- * The this pointer could be a delegated one, -- so when doing the call directly this should be with the non-delgated this (to have a polymorphic call) -- local delegated = this:delegate() -- return delegated[localKey](delegated, unpack(arg)) -- end -- classDef[key] = delegator --else classDef[key] = value --end end end -- copy instances properties assert(classDef.NameToProp) assert(baseClass.NameToProp) for key, prop in pairs(baseClass.Prop) do -- if property not declared in derived class then add it if classDef.NameToProp[prop.Name] == nil then -- its okay to make a reference here because classes definitions are read-only classDef.NameToProp[prop.Name] = prop table.insert(classDef.Prop, prop) end end end -- else no-op end function r2.getLoadedFeaturesStatic() local loadedFeatures = { --Mob Spawners {"r2_features_boss_spawner.lua", "BossSpawnerFeature", "uiR2EdMobSpawnersCategory"}, {"r2_features_timed_spawn.lua", "TimedSpawner", "uiR2EdMobSpawnersCategory"}, {"r2_features_scenery_object_remover.lua", "SceneryObjectRemoverFeature", "uiR2EdMobSpawnersCategory"}, --Chests {"r2_features_easter_egg.lua", "EasterEggFeature", "uiR2EdChestsCategory"}, {"r2_features_random_chest.lua", "RandomChest", "uiR2EdChestsCategory"}, {"r2_features_get_item_from_scenery.lua", "GetItemFromSceneryObject", "uiR2EdChestsCategory"}, --Tasks {"r2_features_give_item.lua", "GiveItemFeature", "uiR2EdTaskStepCategory"}, {"r2_features_talk_to.lua", "TalkToFeature", "uiR2EdTaskStepCategory"}, {"r2_features_request_item.lua", "RequestItemFeature", "uiR2EdTaskStepCategory"}, {"r2_features_visit_zone.lua", "VisitZone", "uiR2EdTasksCategory"}, {"r2_features_target_mob.lua", "TargetMob", "uiR2EdTasksCategory"}, {"r2_features_kill_npc.lua", "KillNpc", "uiR2EdTasksCategory"}, {"r2_features_hunt_task.lua", "HuntTask", "uiR2EdTasksCategory"}, {"r2_features_delivery_task.lua", "DeliveryTask", "uiR2EdTasksCategory"}, {"r2_features_get_item_from_scenery_task.lua", "GetItemFromSceneryObjectTaskStep", "uiR2EdTaskStepCategory"}, {"r2_features_scenery_object_interaction_task.lua", "SceneryObjectInteractionTaskStep", "uiR2EdTaskStepCategory"}, --Triggers {"r2_features_timer.lua", "TimerFeature", "uiR2EdTriggersCategory"}, {"r2_features_zone_triggers.lua", "ZoneTrigger", "uiR2EdTriggersCategory"}, {"r2_features_user_trigger.lua", "UserTriggerFeature", "uiR2EdTriggersCategory"}, {"r2_features_man_hunt.lua", "ManHuntFeature", "uiR2EdTriggersCategory"}, {"r2_features_scenery_object_interaction.lua", "SceneryObjectInteractionFeature", "uiR2EdTriggersCategory"}, {"r2_features_proximity_dialog.lua", "ChatSequence", "uiR2EdTriggersCategory"}, --{"r2_features_reward_provider.lua", "RewardProvider", "uiR2EdTriggersCategory"}, --MacroComponents {"r2_features_ambush.lua", "Ambush", "uiR2EdMacroComponentsCategory"}, {"r2_features_loot_spawner.lua", "LootSpawnerFeature", "uiR2EdMacroComponentsCategory"}, {"r2_features_hidden_chest.lua", "HiddenChest", "uiR2EdMacroComponentsCategory"}, {"r2_features_proximity_dialog.lua", "ProximityDialog", "uiR2EdMacroComponentsCategory"}, {"r2_features_bandits_camp.lua", "BanditCampFeature", "uiR2EdMacroComponentsCategory"}, {"r2_features_fauna.lua", "FaunaFeature", "uiR2EdMacroComponentsCategory"}, } return loadedFeatures end function r2.doFileProtected(filename) local ok, msg = pcall(r2.doFile, filename) if not ok then debugInfo("Error while loading component '"..filename.."' err: "..msg) end end r2.loadFeatures = function() r2.doFileProtected("r2_features_default.lua") r2.doFileProtected("r2_features_npc_groups.lua") r2.doFileProtected("r2_features_counter.lua") r2.doFileProtected("r2_features_reward_provider.lua") --Loading features r2.doFileProtected("r2_features_loaded.lua") local loadedFeatures = r2.getLoadedFeaturesStatic() local k, v = next(loadedFeatures, nil) while k do if v and v[1] then r2.doFileProtected(v[1]) end k, v = next(loadedFeatures, k) end if config.R2EDLoadDynamicFeatures == 1 then local loadedFeatures = r2.getLoadedFeaturesDynamic() local k, v = next(loadedFeatures, nil) while k do if v and v[1] then r2.doFileProtected(v[1]) end k, v = next(loadedFeatures, k) end end r2.doFileProtected("r2_texts.lua") r2.doFileProtected("r2_logic.lua") r2.doFileProtected("r2_logic_entities.lua") r2.doFileProtected("r2_event_handler_system.lua") r2.doFileProtected("r2_unit_test.lua") r2.doFileProtected("r2_core_user_component_manager.lua") --r2_core.UserComponentManager:init() --debugInfo("REGISTERING FEATURES") r2.UserComponentsManager:updateUserComponents() local featureId, feature = next(r2.Features, nil) while (featureId ~= nil) do --debugInfo("Registering feature " .. feature.Name) local componentId, component = next(feature.Components, nil) while (component ~= nil) do --debugInfo(" Registering feature component " .. component.Name) r2.registerComponent(component) componentId, component = next(feature.Components, componentId) end featureId, feature = next(r2.Features, featureId); end end -- Function to init default scenario stuffs, with the given client ID -- tmp : returns ids for the scenario, the first act, and the default group r2.initBaseScenario = function() local function ici(index) -- debugInfo(colorTag(255, 255, 0) .. "ICI " .. tostring(index)) end -- create scenario ici(1) local scenario= r2.newComponent("Scenario") if (scenario == nil) then debugWarning("Failed to create Scenario"); return end ici(2) --debugInfo("Scenario created with id " .. scenario.InstanceId) scenario.title = "TestMap" scenario.shortDescription = "TestMap" scenario.optimalNumberOfPlayer = 1 -- create first act & default feature group do local act =r2.newComponent("Act") act.States = {} if (act == nil) then debugWarning("Failed to create first 'Act'"); return end local features = act.Features local tmpDefault = r2.newComponent("DefaultFeature") if (tmpDefault == nil) then debugWarning("Failed to create default feature"); return end table.insert(features, tmpDefault) table.insert(scenario.Acts, act) end -- By default create act I and have it selected do local act =r2.newComponent("Act") -- force to select the act 1 at display r2.ActUIDisplayer.LastSelfCreatedActInstanceId = act.InstanceId act.States = {} if (act == nil) then debugWarning("Failed to create secondary 'Act'"); return end act.Name = i18n.get("uiR2EDAct1"):toUtf8() act.Title = i18n.get("uiR2EDAct1"):toUtf8() -- obsolete local features = act.Features local tmpDefault = r2.newComponent("DefaultFeature") if (tmpDefault == nil) then debugWarning("Failed to create default feature"); return end table.insert(features, tmpDefault) table.insert(scenario.Acts, act) end r2.requestCreateScenario(scenario) end -- called by the frame work to reset the current scenario -- function r2.resetScenario() -- -- do -- -- r2.requestEraseNode(r2.ScenarioInstanceId, "Acts" ) -- -- -- local acts= {} -- -- do -- local act =r2.newComponent("Act") -- local features = act.Features -- local tmpDefault = r2.newComponent("DefaultFeature") -- table.insert(features, tmpDefault) -- table.insert(acts, act) -- end -- do -- local act =r2.newComponent("Act") -- local features = act.Features -- local tmpDefault = r2.newComponent("DefaultFeature") -- r2.ActUIDisplayer.LastSelfCreatedActInstanceId = act.InstanceId -- act.Title = i18n.get("uiR2EDAct1"):toUtf8() -- table.insert(features, tmpDefault) -- table.insert(acts, act) -- -- table.insert(scenario.Acts, act) -- end -- -- -- r2.requestInsertNode(r2.ScenarioInstanceId, "", -1, "Acts", acts) -- r2.requestReconnection() -- end --end -- called when a gm/ai has do a scheduleStartAct (Animation or test time) function r2.onScheduleStartAct(errorId, actId, nbSeconds) if (r2.Mode == "DM" or r2.Mode == "AnimationModeDm") then if errorId == 0 then local ucStringMsg = ucstring() local str = "Act " .. actId if nbSeconds ~= 0 then str = str .. " will start in " .. nbSeconds .. " seconds" end ucStringMsg:fromUtf8(str) displaySystemInfo(ucStringMsg, "BC") elseif errorId == 1 then messageBox("Act ".. actId .." can not be started because another act is already starting.") elseif errorId == 2 then messageBox("Act ".. actId .." can not be started because this act does not exist.") end end end function r2.onDisconnected() local str = "You have been disconnected by the server." local ucStringMsg = ucstring() messageBox(str) ucStringMsg:fromUtf8(str) displaySystemInfo(ucStringMsg, "BC") end function r2.onKicked(timeBeforeDisconnection, kicked) if kicked then local str = "You have been kicked. You must come back to mainland or leave this session otherwise you will be disconnected in " .. tostring(timeBeforeDisconnection) .. " secondes." local ucStringMsg = ucstring() messageBox(str) ucStringMsg:fromUtf8(str) displaySystemInfo(ucStringMsg, "BC") else local str = "You have been unkicked." local ucStringMsg = ucstring() messageBox(str) ucStringMsg:fromUtf8(str) displaySystemInfo(ucStringMsg, "BC") end end -- called in start mode of a dm function r2.onRuntimeActUpdated(runtimeAct) -- use runtimeAct or r2.getRunTimeActs() r2.AnimGlobals.Acts = runtimeAct -- update the ui r2.ui.AnimBar:update() end function r2.onTalkingAsListUpdated() r2.ui.AnimBar:updateDMControlledEntitiesWindow() end function r2.onIncarnatingListUpdated() r2.ui.AnimBar:updateDMControlledEntitiesWindow() end function r2.onScenarioHeaderUpdated(scenario) local ui=getUI('ui:interface:r2ed_scenario_control') if ui.active == true then ui.active = false ui.active = true end -- inspect(scenario) -- or use r2.getScenarioHeader(); end function r2.onSystemMessageReceived(msgType, msgWho, msg) local ucStringMsg = ucstring() ucStringMsg:fromUtf8(msg) if string.len(msg) > 2 and string.sub(msg, 1, 2) == "ui" then ucStringMsg = i18n.get(msg) msg = ucStringMsg:toString() end if msgType == "BC" or msgType == "BC_ML" then printMsgML(msg) elseif msgType == "SYS" or msgType == "DM" then local str = "" if msgType == "DM" then str = "(AM ONLY)"..str if (r2.Mode ~= "DM" and r2.Mode ~= "AnimationModeDm") then return end end if string.len(msgWho) ~= 0 then str = str .. msgWho .. ": " end str = str.. msg printMsgML(msg) elseif msgType == "ERR" then printMsgML(msg) messageBox(msg) end end -- TMP : place holder function to know the current act if not r2.getCurrentActIndex then debugInfo("Creating place holder for r2.getCurrentActIndex") function r2.getCurrentActIndex() return 1 end end function r2.onUserTriggerDescriptionUpdated(userTrigger) -- use userTrigger or r2.getUserTriggers() r2.AnimGlobals.UserTriggers = userTrigger r2.ui.AnimBar:update() end function r2.onCurrentActIndexUpdated( actIndex) -- actIndex==r2.getCurrentActIndex()) end -- called when a session has begin but no scenario has been created function r2.onEmptyScenarioUpdated() if r2.Mode == "AnimationModeLoading" then UnitTest.testLoadAnimationScenarioUi() elseif r2.Mode == "AnimationModeWaitingForLoading" then UnitTest.testWaitAnimationScenarioLoadingUi() else --UnitTest.testCreateScenarioUi() r2.acts:openScenarioActEditor(true, true) end end -- called by the framework when the scenario has been updated function r2.onScenarioUpdated(scenario, startingActIndex) --luaObject(scenario) --breakPoint() if (scenario == nil) then r2.onEmptyScenarioUpdated() return else hide('ui:interface:r2ed_form_CreateNewAdventureStep2') end r2.Scenario = r2:getInstanceFromId(scenario.InstanceId) r2.ScenarioInstanceId = scenario.InstanceId -- add permanent nodes to act node r2:defaultUIDisplayer():addPermanentNodes() if r2.Version.updateVersion() then r2.setScenarioUpToDate(true) else r2.setScenarioUpToDate(false) end local currentAct = nil assert(startingActIndex); assert( type(startingActIndex) == "number"); if startingActIndex < table.getn(scenario.Acts) then r2.DefaultActInstanceId = scenario.Acts[startingActIndex].InstanceId r2.ActUIDisplayer.LastSelfCreatedActInstanceId = scenario.Acts[startingActIndex].InstanceId if scenario.Acts[startingActIndex].Features.Size > 0 then r2.DefaultFeatureInstanceId = scenario.Acts[startingActIndex].Features[0].InstanceId end currentAct=scenario.Acts[startingActIndex] r2.ScenarioWindow:setAct(currentAct) else r2.DefaultActInstanceId = scenario.Acts[0].InstanceId r2.ActUIDisplayer.LastSelfCreatedActInstanceId = scenario.Acts[0].InstanceId if scenario.Acts[0].Features.Size > 0 then r2.DefaultFeatureInstanceId = scenario.Acts[0].Features[0].InstanceId end currentAct=scenario.Acts[0] end if scenario ~= nil and currentAct ~= nil then r2.Scenario.User.SelectedActInstanceId = tostring(currentAct.InstanceId) r2.Scenario.User.SelectedLocationInstanceId = tostring(currentAct.LocationId) end r2.ScenarioWindow:updateScenarioProperties() -- usefull to know if the scenario is updating ld.lock = 0 if not r2.RingAccess.LoadAnimation and not r2.getIsAnimationSession() then local ok, level, err = r2.RingAccess.verifyScenario() r2.updateScenarioAck(ok, level, err.What) return end r2.acts.deleteOldScenario = false if r2.getUseVerboseRingAccess() then r2.RingAccess.dumpRingAccess() end end function r2.verifyScenario() local ok, level, err = r2.RingAccess.verifyScenario() local msg="" if not ok then printMsg(err.What) msg = err.What end return ok, msg end function r2.printMsg(msg) r2.printMsg(msg) end -- assign default menu for each classes function r2.initDefaultMenuSetup() forEach(r2.Classes, function(k, v) if v.Menu ~= nil and v.onSetupMenu == nil then v.onSetupMenu = r2.defaultMenuSetup end end ) end -- assign default menu for each classes function r2.initDefaultPropertyDisplayer() for k, class in pairs(r2.Classes) do if class.BuildPropertySheet == true then if class.DisplayerProperties == nil then class.DisplayerProperties = "R2::CDisplayerLua" class.DisplayerPropertiesParams = "propertySheetDisplayer" end end end end -- setup the classes function r2.setupClasses() -- first build a table that gives access to a property from its name for k, class in pairs(r2.Classes) do class.NameToProp = {} for k, prop in pairs(class.Prop) do if prop.Name == nil then debugInfo("Found a property in class " .. tostring(class.Name) .. " with no field 'Name'") end class.NameToProp[prop.Name] = prop end end -- perform subclassing for k, class in pairs(r2.Classes) do r2.Subclass(class) end -- register into C for k, class in pairs(r2.Classes) do r2.registerGenerator(class) end end -- returns a table which map each instanceId of the scenario component's to each component r2.createComponentsMap = function (scenario) function createComponentsMapImpl (t, components) if ( type(t) == "table") then if (t.InstanceId ~= nil) then components[t.InstanceId] = t end for key, value in pairs(t) do createComponentsMapImpl(value, components) end end end local components = {} createComponentsMapImpl(scenario, components) return components end r2.updateActCost = function(act) assert(act) local cost = 0 local staticCost = 0 local features = act.Features assert(features ~= nil ) local featureId, feature = next(features, nil) while (featureId ~= nil) do -- feature:getCost() is obsolete if feature.User.GhostDuplicate ~= true then if feature and feature.getAiCost then local added = feature:getAiCost() if added then cost = cost + added end end if feature and feature.getStaticObjectCost then local added = feature:getStaticObjectCost() if added then staticCost = staticCost + added end end end featureId, feature = next(features, featureId) end -- NB nico : removed cost from the real object and put is in the 'User' table (interfere with undo redo, because considered -- as an action) act:setLocalCost(cost) --if (act.Cost ~= cost) then -- r2.requestSetLocalNode(act.InstanceId, "Cost", cost) -- r2.requestCommitLocalNode(act.InstanceId, "Cost") --end act:setLocalStaticCost(staticCost) --if (act.StaticCost ~= staticCost) then -- r2.requestSetLocalNode(act.InstanceId, "StaticCost", staticCost) -- r2.requestCommitLocalNode(act.InstanceId, "StaticCost") --end end r2.registerText = function(text) --TODO : when several texts are registered "at the same time", the local scenario --has not the time to receive the changes, and a new entry is created. local checkText = r2.Features["TextManager"].checkText local textMgr = getTextMgr() if(textMgr==nil) then debugInfo("text mgr nil!!") end local result = checkText(textMgr,text) if result.Count ~= 0 then --the entry already exist, just increment the counter r2.requestSetNode(result.InstanceId,"Count",result.Count+1) --temporaire --result.Count = result.Count + 1 --/temporaire debugInfo("Entry already exist") else --the entry don't exist, insert it result.Count=1 -- debugInfo("New entry created") r2.requestInsertNode(r2.Scenario.Texts.InstanceId,"Texts",-1,"",result) --temporaire --table.insert(r2.TextMgr.Texts,result) --temporaire end return result end getTextMgr = function() --return r2.TextMgr return r2.Scenario.Texts end r2.unregisterText = function(text) local removeText = r2.Features["TextManager"].removeText removeText(r2.Scenario.Texts,text) end r2.unregisterTextFromId = function(id) local text = r2.getText(id) if text ~= nil then r2.unregisterText(text) end end r2.getText = function(id) local textMgr = getTextMgr() return r2.Features["TextManager"].getText(textMgr, id) end r2.split = function(str, sep) assert( type(str) == "string") local ret = {} local start=0 if sep == nil then sep = "\n" end local fin=string.find(str, sep) while fin ~= nil do local tmp = string.sub(str,start,fin-1) if string.len(tmp)~=0 then table.insert(ret,tmp) end start = fin+1 fin = string.find(str,sep,start) end if start scrH then -- localH = scrH --end --uiInfo.h = localH --uiInfo.pop_min_h= localH --uiInfo.pop_max_h= localH uiInfo:invalidateCoords() uiInfo:updateCoords() uiInfo:center() setTopWindow(uiInfo) end function r2.setFeatureDisplayHelp() local checkBox = getUI("ui:interface:feature_help:content:custom_bbox_enabled") assert(checkBox) local isChecked = checkBox.pushed debugInfo("checked: " ..tostring(isChecked)) local ui = getUI("ui:interface:feature_help") local name = ui.Env.uc_title local len = string.len(name) - 10 - 6 local className = string.sub(name, -10-len, 6+len) --removing uiR2Ed and _HelpTitle --formName = formName .."Form" assert(className) --debugInfo("Form name: " ..formName) if isChecked == false then r2.setDisplayInfo(className, 1) else r2.setDisplayInfo(className, 0) end end function r2.getDisplayButtonHeader(func, buttonText) local header = string.format( [[ ]], func, buttonText) return header end function r2.updateLogicEvents(this, invalidEvents) assert(invalidEvents) assert(this) local actions = this.Behavior.Actions assert(actions) local function updateLogicEvent(k, action) local event = action.Event assert(event) if invalidEvents[event.Type] then local instanceId = tostring(event.InstanceId) r2.requestSetNode(instanceId, "Type", tostring(invalidEvents[event.Type])) end end forEach(actions, updateLogicEvent) end function r2.updateLogicActions(this, invalidActions, entityClass) assert(invalidActions) assert(this) assert(entityClass) local instance = r2:getInstanceFromId(this.Entity) if not instance or not instance:isKindOf(entityClass) then return end local action = this.Action assert(action) if invalidActions[action.Type] then r2.requestSetNode(action.InstanceId, "Type", invalidActions[action.Type]) end end function r2.onRingAccessUpdated(access) r2:buildPaletteUI() r2.acts:initActsEditor() updateBanditCampEnum() -- also available by r2.getRingAccess() end function r2:checkAiQuota(size) if not size then size = 1 end local leftQuota, leftAIQuota, leftStaticQuota = r2:getLeftQuota() if leftAIQuota < size then displaySystemInfo(i18n.get("uiR2EDMakeRoomAi"), "BC") return false end return true end function r2:checkStaticQuota(size) if not size then size = 1 end local leftQuota, leftAIQuota, leftStaticQuota = r2:getLeftQuota() if leftStaticQuota < size then displaySystemInfo(i18n.get("uiR2EDMakeRoomStaticObject"), "BC") return false end return true end function r2.DisplayNpcHeader() local npc = r2:getSelectedInstance() if not npc then return "" end if npc:isGrouped() then local header = [[ ]] return header else return "" end end function r2.mustDisplayProp(prop) end