├── 0.7 ├── CellReset │ ├── CellReset.lua │ └── readme.md ├── GeneralGUI │ ├── GeneralGUI.lua │ ├── generalGuiExample.lua │ └── readme.md ├── HotkeysExpanded │ ├── HotkeysExpanded.lua │ └── readme.md ├── ServerWarp │ ├── README.md │ └── ServerWarp.lua ├── classInfo.lua ├── decorateHelp.lua ├── flatModifiers.lua ├── kanaBank │ ├── kanaBank.lua │ └── readme.md ├── kanaFurniture │ └── kanaFurniture.lua ├── kanaHousing │ ├── kanaHousing.lua │ └── readme.md ├── kanaMOTD │ ├── kanaMOTD.json │ ├── kanaMOTD.lua │ └── readme.md ├── kanaRevive │ ├── kanaRevive.lua │ └── readme.md ├── kanaStartingItems │ ├── kanaStartingItems.lua │ └── readme.md └── readme.md ├── 0.8 ├── kanaBank │ └── kanaBank.lua ├── kanaRevive │ └── kanaRevive.lua └── readme.md ├── CellReset.lua ├── LICENSE.md ├── Porting Scripts to 0.7 alpha and How Does.md ├── README.md ├── Random Assortment of tes3mp Scripting Knowledge.md ├── Unofficial Guide to tes3mp.md ├── bannedEquipment.lua ├── classCap.lua ├── classInfo.lua ├── decorateHelp.lua ├── decoratorsAid.lua ├── flatModifiers.lua ├── flatModifiersBasic.lua ├── itemInfo.lua ├── kanaFurniture.lua ├── kanaHousing ├── README.md └── kanaHousing.lua ├── markRecall.lua ├── salesChest.lua ├── salesChestGlobalHack.lua ├── serverWarp.lua └── tes3mp FAQ.md /0.7/CellReset/readme.md: -------------------------------------------------------------------------------- 1 | # CellReset 2 | Cell Reset is a script for periodically resetting the game's cells to their default states, in a manner that tries to avoid all the potential problems other methods might cause (for example, manually deleting a cell's `.json` entry can lead to problems!). The server owner can configure how often these happen, as well as provide a list of cells that they don't want to be affected. 3 | 4 | *Currently written for a version of 0.7-alpha* 5 | 6 | ## Usage 7 | By default, the script is supposed to work automatically, resetting the state of any cell as it's loaded after 3 IRL days (time can be configured) have passed since its last reset (or first loaded if it hasn't been reset at all). With some configuration, the automatic resetting can be disabled, and instead left to other scripts, or admins to handle via the `/forceReset` command. Here are the commands added by Cell Reset: 8 | - `/resetTime Cellname` - Reports how much more time is left on the given cell's reset timer. If no cell name is provided, it'll use the command user's current cell instead. 9 | - `/forceReset Cellname` - Forcibly resets the given cell (or the command user's current cell if none is provided). **Only use if you know what you're doing** - all the regular checks are bypassed when using this command, so this will wipe cells listed as being exempt from resetting, for example. By default, anyone who had that cell loaded in their client memory will be kicked from the server when the command is used (see *configuration* and *Known Issues/Limitations*) 10 | 11 | ## Configuration 12 | Configuration can be done from within the file itself. Here is a list of all the current config options: 13 | - **scriptConfig.resetTime** - The time in (real life) seconds that must've passed before a cell is attempted to be reset. 259200 seconds is 3 days. Set to -1 to disable automatic resetting. 14 | - **scriptConfig.preserveCellChanges** - If true, the script won't reset actors that that have moved into/from the cell. At the moment, MUST be true. 15 | - **scriptConfig.alwaysPreservePlaced** - If true, the script will always preserve any placed objects, even in cells that it's free to delete from. Note that *place*d is a tes3mp technical term, referring to any object that's been physically placed in the world by a player/script (and didn't start there). Items put inside containers *aren't* considered *place*d objects. 16 | - **scriptConfig.blacklist** - The blacklist contains a list of every cell that you want to be immune to automatic cell resets. Note that this doesn't protect them from being reset via the `/forcereset` command, because that bypasses such checks. 17 | - **scriptConfig.checkResetTimeRank** - The `staffRank` required by a player to use the `/resetTime` command. 18 | - **scriptConfig.forceResetRank** - The `staffRank` required by a player to use the `/forceReset` command. 19 | - **scriptConfig.kickAffectedPlayersAfterForceReset** - If true, players that had information on a cell in their client memory will be kicked following a force reset. Should be set to true or problems will arise! 20 | - **scriptConfig.logging** - If true, the script outputs basic information to the log. No real reason why you'd want to disable it. 21 | - **scriptConfig.debug** - If true, the script outputs additional debug information to the log. 22 | 23 | ## Installation 24 | ### Save the Script 25 | Save the file as `CellReset.lua` inside your `server/scripts/custom` folder. 26 | ### Edits to `customScripts.lua` 27 | - CellReset = require("custom.CellReset") 28 | 29 | ## Script Methods 30 | There are a number of functions made available for other scripts to utilise. See the file for more information on what each of them do. 31 | 32 | ## Known Issues/Limitations 33 | I've done my best to make sure that everything works as it should, but problems can get past testing. If you run into any problems while running it, please do get in touch and I'll try my best to fix it. 34 | ### Limitations 35 | Because of the way tes3mp works, there are limitations to when a cell can be reset. The main limiting factor is Client Memory: If a player (or somebody else) has ever entered a cell during the player's current session, their client will hold information about every change that was made. Without making some edits to the server scripts (and adding the requirement that this script must've *always* been running on the server since the start), there isn't a way to "undo" the things that client has received. (I'm struggling to articulate exactly how everything works on a technical level, so rather than sit around trying to do that, I'd rather skip this and actually finish releasing this script :P. Maybe something mentioned [in this guide](https://github.com/Atkana/tes3mp-scripts/blob/master/Unofficial%20Guide%20to%20tes3mp.md) will help? I don't remember.) 36 | 37 | Additionally, the information about actors changing to or from cells is always preserved between resets. Because this script allows for staggered resets, as well as some cells to be entirely reserved, it can't guarantee its okay to reset that actor (otherwise we might end up with multiple actor objects that share the exact same ID!). Technically, the script could potentially work around that obstacle, but the implementation would have to bake-in the assumption that this script is the ultimate and only authority for cell resetting running on the server, and I'd rather avoid that. 38 | 39 | ## What are the problems with other methods? 40 | Unless you're deleting most of your server data - all your cell `.json`s, and all your `recordstore` entries - and doing so all in one go, you'll run into problems (albeit very niche). At the moment these are namely related to *cell changes* and *record links*. 41 | ### Cell Changes 42 | When a data file actor changes cells, a note is made in the cell they left from telling them what cell they moved to, and a note is made in the new cell telling them where they originally came from. If you aren't deleting every cell file altogether at the same time, you might run into an instance where you've reset the cell that the actor has left from, but preserved the cell the actor moved to. In this case, there will be two actors with the same `uniqueIndex`, which can cause a lot of wonkiness. If you are manually deleting cell files, make sure all the ones that you preserve don't contain actors with `cellChangeFrom` information about cells that you deleted. 43 | ### Record Links 44 | When a custom-made item is placed inside a cell, a link is created between that custom item's information in `recordstore` and the cell. If the item is removed from a cell, that link is then removed, and at the time of unlinking a check is made - if that link was the last one to that item, then the information on that item is deleted from the `recordstore`. If the cell file is simply deleted, the links inside `recordstore` are still recorded for that cell, and so without the unlinking, items that no longer have any existing instances can still exist inside `recordstore`, causing some unnecessary save bloating (although this is unlikely to be that big of a problem). If you do manually delete a cell file, you need to also manually update all the information in each respective `recordstore` entry (which is a faff, trust me :P). 45 | -------------------------------------------------------------------------------- /0.7/GeneralGUI/GeneralGUI.lua: -------------------------------------------------------------------------------- 1 | -- GeneralGUI - Release 1 - For tes3mp 0.7-prerelease 2 | -- Very Basic GUI API for structuring custom GUIs 3 | -- Note that this hasn't been fully tested, and is subject to change 4 | 5 | local GeneralGUI = {} 6 | 7 | GeneralGUI.registered = {} 8 | GeneralGUI.GUIids = {["CustomMessageBox"] = 2500, ["InputDialog"] = 2501, ["ListBox"] = 2502} 9 | GeneralGUI.currentChainData = {} 10 | 11 | GeneralGUI.OnGUIAction = function(pid, idGui, data) 12 | for guiType, id in pairs(GeneralGUI.GUIids) do 13 | if idGui == id then 14 | local chainData = GeneralGUI.GetChainData(pid) 15 | local guiInfo = GeneralGUI.registered[chainData.currentGeneralGuiId] 16 | 17 | if guiType == "CustomMessageBox" then 18 | local chosenIndex = tonumber(data) + 1 19 | 20 | local selectedChoiceData = chainData.currentChoiceList[chosenIndex] 21 | 22 | if selectedChoiceData.callback ~= nil then 23 | selectedChoiceData.callback(chainData, selectedChoiceData) 24 | else 25 | if guiInfo.OnSelectOption ~= nil then 26 | guiInfo.OnSelectOption(chainData, selectedChoiceData) 27 | else 28 | GeneralGUI.Error("OnSelectOption missing for " .. chainData.currentGeneralGuiId) 29 | end 30 | end 31 | 32 | return true 33 | elseif guiType == "InputDialog" then 34 | local input = tostring(data) or "" 35 | 36 | -- Perform validation check if available 37 | if guiInfo.ValidateInput ~= nil then 38 | local success, message = guiInfo.ValidateInput(chainData, input) 39 | if not success then 40 | local rejectMessage = message or "Please enter valid text." 41 | 42 | tes3mp.MessageBox(pid, -1, rejectMessage) 43 | return true, GeneralGUI.ReshowLast(chainData) 44 | end 45 | end 46 | 47 | if guiInfo.OnInput ~= nil then 48 | guiInfo.OnInput(chainData, input) 49 | else 50 | GeneralGUI.Error("OnInput missing for " .. chainData.currentGeneralGuiId) 51 | end 52 | 53 | return true 54 | elseif guiType == "ListBox" then 55 | local rawChoice = tonumber(data) 56 | 57 | -- Enforce required selection if GUI required 58 | if guiInfo.requireSelection ~= nil and guiInfo.requireSelection and rawChoice == 18446744073709551615 then 59 | local rejectMessage = guiInfo.requireSelectionRejectMessage or "Please make a valid selection." 60 | 61 | tes3mp.MessageBox(pid, -1, rejectMessage) 62 | return true, GeneralGUI.ReshowLast(chainData) 63 | end 64 | 65 | local chosenIndex = rawChoice + 1 66 | local selectedChoiceData = chainData.currentChoiceList[chosenIndex] 67 | 68 | if selectedChoiceData.callback ~= nil then 69 | selectedChoiceData.callback(chainData, selectedChoiceData) 70 | else 71 | if guiInfo.OnSelectOption ~= nil then 72 | guiInfo.OnSelectOption(chainData, selectedChoiceData) 73 | else 74 | GeneralGUI.Error("OnSelectOption missing for " .. chainData.currentGeneralGuiId) 75 | end 76 | end 77 | 78 | return true 79 | end 80 | end 81 | end 82 | end 83 | 84 | GeneralGUI.RegisterGUI = function(id, data) 85 | GeneralGUI.registered[string.lower(id)] = data 86 | end 87 | 88 | GeneralGUI.Error = function(message) 89 | tes3mp.LogMessage(1, "[GeneralGUI] ERROR: " .. message) 90 | end 91 | 92 | -- Start a new chain. Should be called before ShowGUI when starting a new sequence of GUIs (even if there's only one) 93 | GeneralGUI.StartChain = function(pid) 94 | -- Use pid or name? 95 | GeneralGUI.currentChainData[pid] = {["pid"] = pid} 96 | end 97 | 98 | GeneralGUI.GetChainData = function(pid) 99 | return GeneralGUI.currentChainData[pid] --Or create if missing? 100 | end 101 | 102 | -- Empty chain data 103 | GeneralGUI.EndChain = function(pid) 104 | GeneralGUI.currentChainData[pid] = nil 105 | end 106 | 107 | -- Generic function that you can use as the callback for your close buttons. 108 | GeneralGUI.CloseButton = function(chainData) 109 | GeneralGUI.EndChain(chainData.pid) 110 | end 111 | 112 | -- Re-show the current GUI (technically, the last one that was open) 113 | GeneralGUI.ReshowLast = function(chainData) 114 | GeneralGUI.ShowGUI(chainData.pid, chainData.currentGeneralGuiId) 115 | end 116 | 117 | GeneralGUI.ShowGUI = function(pid, id) 118 | local id = string.lower(id) 119 | if not GeneralGUI.registered[id] then 120 | -- Not found. Abort 121 | GeneralGUI.Error("Attempted to show non-registered GUI " .. id) 122 | return false 123 | end 124 | local guiInfo = GeneralGUI.registered[id] 125 | local chainData = GeneralGUI.GetChainData(pid) 126 | 127 | -- Abort if chain data is missing 128 | if chainData == nil then 129 | GeneralGUI.Error("Attempted to load " .. id .. " without chain data!") 130 | return false 131 | end 132 | 133 | chainData.currentGeneralGuiId = id 134 | 135 | if guiInfo.type == "CustomMessageBox" then -- Custom Message Box 136 | -- Label 137 | local label 138 | if guiInfo.GenerateLabel ~= nil then 139 | label = guiInfo.GenerateLabel(chainData) 140 | elseif guiInfo.label ~= nil then 141 | label = guiInfo.label 142 | else 143 | label = "" 144 | end 145 | 146 | -- Buttons 147 | local buttonDataList 148 | if guiInfo.GenerateChoices ~= nil then 149 | buttonDataList = guiInfo.GenerateChoices(chainData) 150 | else -- Create default if GUI info is lacking Generate function 151 | buttonDataList = {{index = 1, display = "Close", id = "close", callback = GeneralGUI.CloseButton}} 152 | end 153 | 154 | local buttonString = "" 155 | for index = 1, #buttonDataList do 156 | buttonString = buttonString .. buttonDataList[index].display 157 | if buttonDataList[index+1] ~= nil then 158 | buttonString = buttonString .. ";" 159 | end 160 | end 161 | 162 | chainData.currentChoiceList = buttonDataList 163 | 164 | -- Display GUI 165 | return tes3mp.CustomMessageBox(pid, GeneralGUI.GUIids.CustomMessageBox, label, buttonString) 166 | elseif guiInfo.type == "ListBox" then -- List Box 167 | -- Label 168 | local label 169 | if guiInfo.GenerateLabel ~= nil then 170 | label = guiInfo.GenerateLabel(chainData) 171 | elseif guiInfo.label ~= nil then 172 | label = guiInfo.label 173 | else 174 | label = "" 175 | end 176 | 177 | -- Choices 178 | local choiceDataList 179 | if guiInfo.GenerateChoices ~= nil then 180 | choiceDataList = guiInfo.GenerateChoices(chainData) 181 | else -- Create default if GUI info is lacking Generate function 182 | choiceDataList = {{index = 1, display = "*Close*", id = "close", callback = GeneralGUI.CloseButton}} 183 | end 184 | 185 | local choiceString = "" 186 | for index = 1, #choiceDataList do 187 | choiceString = choiceString .. choiceDataList[index].display 188 | if choiceDataList[index+1] ~= nil then 189 | choiceString = choiceString .. "\n" 190 | end 191 | end 192 | 193 | chainData.currentChoiceList = choiceDataList 194 | 195 | -- Display GUI 196 | return tes3mp.ListBox(pid, GeneralGUI.GUIids.ListBox, label, choiceString) 197 | elseif guiInfo.type == "InputDialog" then -- Input Dialog 198 | -- Label 199 | local label 200 | if guiInfo.GenerateLabel ~= nil then 201 | label = guiInfo.GenerateLabel(chainData) 202 | elseif guiInfo.label ~= nil then 203 | label = guiInfo.label 204 | else 205 | label = "" 206 | end 207 | 208 | -- Note 209 | local note 210 | if guiInfo.GenerateNote ~= nil then 211 | note = guiInfo.GenerateNote(chainData) 212 | elseif guiInfo.note ~= nil then 213 | note = guiInfo.note 214 | else 215 | note = "" 216 | end 217 | 218 | -- Display GUI 219 | if guiInfo.isPassword then 220 | return tes3mp.PasswordDialog(pid, GeneralGUI.GUIids.InputDialog, label, note) 221 | else 222 | return tes3mp.InputDialog(pid, GeneralGUI.GUIids.InputDialog, label, note) 223 | end 224 | end 225 | end 226 | 227 | ------------- 228 | return GeneralGUI 229 | -------------------------------------------------------------------------------- /0.7/GeneralGUI/generalGuiExample.lua: -------------------------------------------------------------------------------- 1 | -- An example for GeneralGUI 2 | -- Once installed, use /guitest in chat to open the sequence 3 | 4 | --[[ INSTALLATION 5 | = GENERAL = 6 | a) Save this file as "generalGuiExample.lua" in mp-stuff/scripts 7 | 8 | = IN SERVERCORE.LUA = 9 | a) Find the line [ menuHelper = require("menuHelper") ]. Add the following BENEATH it: 10 | [ generalGuiExample = require("generalGuiExample") ] 11 | 12 | = IN COMMANDHANDLER.LUA = 13 | a) Find the section: 14 | [ else 15 | local message = "Not a valid command. Type /help for more info.\n" ] 16 | Add the following ABOVE it: 17 | [ elseif cmd[1] == "guitest" then generalGuiExample.OnCommand(pid) ] 18 | ]] 19 | local Methods = {} 20 | 21 | GeneralGUI = require("GeneralGUI") 22 | 23 | --============= 24 | -- ExampleStart GUI 25 | --============= 26 | --[[ 27 | For this GUI, we'll be creating a prompt for players to enter a number. 28 | We'll record that number to use in the next GUI, which we'll display after something is entered into this one 29 | Because we want a number, we won't allow the player to progress until they enter one 30 | Because this number will be super secret, we'll have the GUI hide what they're entering, like is done when somebody is entering a password 31 | ]] 32 | 33 | -- This is what we're using as out validator. 34 | -- Since we only want numbers, we'll return false if the player doesn't provide one 35 | local function firstExampleValidateInput(chainData, input) 36 | if not tonumber(input) then 37 | return false, "Please input a number" 38 | else 39 | return true 40 | end 41 | end 42 | 43 | -- This is what we'll use as our OnInput. 44 | -- It'll be run after the player inputs something which passes the validator's tests. 45 | local function firstExampleOnInput(chainData, input) 46 | -- Let's store the number that the player inputted into the chain data, so we can use it later 47 | chainData.inputtedNumber = tonumber(input) 48 | -- And let's show the next GUI in the sequence. 49 | -- (So far we haven't created it in the code - we'll get to it later) 50 | return GeneralGUI.ShowGUI(chainData.pid, "examplesecond") 51 | end 52 | 53 | -- This is what we'll use to generate our label 54 | local function firstExampleGenerateLabel(chainData) 55 | -- Spoilers: Because we are able to revisit this GUI later on in the sequence, 56 | -- It's possible that we have an inputtedNumber in the chain data! 57 | -- (Normally it's *this* GUI which adds that into the chain data) 58 | -- So in the case that we have returned here, we add a special message 59 | if chainData.inputtedNumber then 60 | return "Last time you were here you put " .. chainData.inputtedNumber 61 | else 62 | return "" -- Otherwise we just give a blank string to use as our label 63 | end 64 | end 65 | 66 | -- Here's what a structured GUI Data looks like 67 | local firstGUIData = { 68 | type = "InputDialog", --The type is InputDialog so that the GUI is an InputDialog... 69 | isPassword = true, --This special flag will mean we use a password dialog 70 | GenerateLabel = firstExampleGenerateLabel, 71 | note = "Enter a secret number", --Because we haven't provided a GenerateNote, this string will be used for the GUI's note 72 | ValidateInput = firstExampleValidateInput, 73 | OnInput = firstExampleOnInput 74 | } 75 | 76 | -- Now, we simply register the GUI 77 | GeneralGUI.RegisterGUI("examplestart", firstGUIData) 78 | 79 | --============== 80 | -- ExampleSecond GUI 81 | --============== 82 | --[[ 83 | For this GUI, we'll be creating a series of button options for the player to choose. 84 | The player will have buttons to: 85 | - Go back to the first GUI and enter a new number 86 | - Open the next GUI (a list of choices) 87 | - Close the GUI, ending the sequence 88 | Additionally, we'll display an extra special button if the player entered 1337 as their number previously 89 | Because we want to mess around a bit, we'll make the text in the box change based on what the player has been doing, exploiting our knowledge that they can go back to the first GUI 90 | ]] 91 | 92 | -- This is what we'll use to generate our label 93 | local function secondExampleGenerateLabel(chainData) 94 | -- The message that we'll use will change depending on whether we've been 95 | -- here before in our current sequence, making reference to our changes 96 | local message = "" 97 | if not chainData.sawNumber then 98 | message = "You entered the number " .. chainData.inputtedNumber 99 | elseif chainData.inputtedNumber ~= chainData.sawNumber then 100 | message = "Hey, you went back and changed your number from " .. chainData.sawNumber .. " to " .. chainData.inputtedNumber .. "!" 101 | elseif chainData.inputtedNumber == chainData.sawNumber then 102 | message = "Look, don't waste my time by going back and entering the same number again..." 103 | end 104 | 105 | -- Here, we store that last input number that we displayed here to use later 106 | -- on for this very function if this is used later... Weird how time works. 107 | chainData.sawNumber = chainData.inputtedNumber 108 | return message 109 | end 110 | 111 | -- This is what'll be run if a player selects the "Go Back" button 112 | local function secondGoBack(chainData) 113 | return GeneralGUI.ShowGUI(chainData.pid, "examplestart") 114 | end 115 | 116 | -- This is what'll be run if a player selects the "Choose a Thing" 117 | local function secondSeeList(chainData) 118 | return GeneralGUI.ShowGUI(chainData.pid, "examplethird") 119 | end 120 | 121 | -- We'll use this as our OnSelectOption 122 | -- Note that because we'll be providing callbacks for every choice bar one, this'll only 123 | -- be called when a player selects the secret elite button (the only one without a callback) 124 | local function secondExampleOnSelectOption(chainData, choiceData) 125 | -- Despite this only being called for the secret elite button, we'll do a check anyways... 126 | if choiceData.id == "elite" then 127 | tes3mp.MessageBox(chainData.pid, -1, "You are super elite!") 128 | GeneralGUI.EndChain(chainData.pid) 129 | end 130 | end 131 | 132 | -- We'll usre this as our GenerateChoices 133 | local function secondExampleGenerateChoices(chainData) 134 | local optionOrder = {"back", "elite", "choice", "close"} -- We'll use this to determine the order the options appear in. 135 | local optionData = { 136 | back = {display = "Go back", id = "back", callback = secondGoBack}, 137 | elite = {display = "Secret Elite button", id = "elite"}, 138 | close = {display = "Close", id = "close", callback = GeneralGUI.CloseButton}, 139 | choice = {display = "Choose a thing", id = "choose", callback = secondSeeList} 140 | } 141 | 142 | local currentIndex = 1 143 | 144 | local out = {} --This table will ultimately be our choice list 145 | 146 | -- In this example, we'll be going through each possible choice that could be presented 147 | -- and determining which ones to present to the player. 148 | for _, id in ipairs(optionOrder) do 149 | -- The default assumption in this instance is that we add each button 150 | 151 | -- Here we do a check to determine if we add the "elite" button 152 | -- It should only appear to a player if they entered "1337" as their number 153 | -- If they didn't then we skip adding the "elite" button 154 | if id == "elite" and chainData.inputtedNumber ~= 1337 then 155 | -- don't add secret elite button 156 | else 157 | out[currentIndex] = {display = optionData[id].display, id = optionData[id].id, callback = optionData[id].callback or nil, index = currentIndex} 158 | currentIndex = currentIndex + 1 159 | end 160 | end 161 | 162 | return out 163 | end 164 | 165 | -- Heres the data for this GUI 166 | local secondGUIData = { 167 | type = "CustomMessageBox", 168 | GenerateLabel = secondExampleGenerateLabel, 169 | GenerateChoices = secondExampleGenerateChoices, 170 | OnSelectOption = secondExampleOnSelectOption, 171 | } 172 | 173 | -- Register it! 174 | GeneralGUI.RegisterGUI("examplesecond", secondGUIData) 175 | 176 | --============= 177 | -- ExampleThird GUI 178 | --============= 179 | --[[ 180 | For this GUI, we'll be prompt players to pick the best thing from a list. 181 | Because we want to force the players to pick an option, we'll use the correct flag, and create a message telling them off for whimping out and trying to avoid expressing an opinion! 182 | The list of choices will always be the same, so we don't have to do anything fancy to generate them. 183 | We'll use one callback for when players make the right choice, and then use the default fallback of OnSelectOption for all the wrong choices. 184 | ]] 185 | 186 | -- Here's what'll happen if the player picks "Skooma" from this GUI's choices 187 | local function thirdChooseSkooma(chainData) 188 | tes3mp.MessageBox(chainData.pid, -1, "The correct choice!") 189 | tes3mp.PlaySpeech(chainData.pid, "vo\\k\\m\\Idl_KM001.mp3") 190 | GeneralGUI.EndChain(chainData.pid) 191 | end 192 | 193 | -- Unlike in the second GUI, the sample of choices won't change 194 | -- so we can just define a choice list here 195 | local thirdChoices = { 196 | {index = 1, display = "Skooma", id = "skooma", callback = thirdChooseSkooma}, 197 | {index = 2, display = "Fargoth", id = "fargoth"}, 198 | {index = 3, display = "Cliff Racers", id = "racers"}, 199 | } 200 | -- We'll use this as our GenerateChoices. All it does is return the above table 201 | local function thirdGenerateChoices() 202 | return thirdChoices 203 | end 204 | 205 | -- We'll use this as our OnSelectOption. 206 | -- It only gets executed if the "wrong choices" are picked, because skooma has its own callback 207 | local function thirdOnSelect(chainData, choiceData) 208 | tes3mp.MessageBox(chainData.pid, -1, "BOO! " .. choiceData.display .. " is the wrong choice!") 209 | 210 | GeneralGUI.EndChain(chainData.pid) 211 | end 212 | 213 | -- Here's the data for this GUI 214 | local thirdGUIData = { 215 | type = "ListBox", 216 | label = "Pick the thing which is best.", --Because there's no GenerateLabel, this'll be used for the label instead. 217 | GenerateChoices = thirdGenerateChoices, 218 | OnSelectOption = thirdOnSelect, 219 | requireSelection = true, --Because we want to force the player to pick from the list, we use this 220 | requireSelectionRejectMessage = "Pick something, fool!", --This is a special message that the player will be shown if they don't make a choice 221 | } 222 | 223 | -- Register it! 224 | GeneralGUI.RegisterGUI("examplethird", thirdGUIData) 225 | 226 | 227 | Methods.OnCommand = function(pid) 228 | GeneralGUI.StartChain(pid) 229 | GeneralGUI.ShowGUI(pid, "examplestart") 230 | end 231 | 232 | return Methods 233 | -------------------------------------------------------------------------------- /0.7/GeneralGUI/readme.md: -------------------------------------------------------------------------------- 1 | # GeneralGUI 2 | This is just some generalised way of using tes3mp's GUI stuff that I'll most likely use for my scripts from now on. It doesn't make using them any easier (in fact, it makes more work), however it offers a basic structure to use. I'd initially intended to simply insert this into every script I needed to use it in rather than making it into a fully-fledge module in order to cut down on installation steps, but either way requires at least some installation, so I relented :P (Though I may still use it the former way, anyways) 3 | 4 | ## Installation 5 | ### General 6 | - Save the file as `GeneralGUI.lua` in `mp-stuff/scripts` 7 | ### In `serverCore.lua` 8 | - Find the line `menuHelper = require("menuHelper")`. Add the following *beneath* it: ```GeneralGUI = require(GeneralGUI)``` 9 | - Find the line `if eventHandler.OnGUIAction(pid, idGui, data) then return end`. Add the following *beneath* it: ```if GeneralGUI.OnGUIAction(pid, idGui, data) then return end``` 10 | 11 | ## Usage 12 | ### Register a New GUI 13 | In order to register a new GUI, use `GeneralGUI.RegisterGUI(id, data)`. `id` should be the identifier you intend to use for your GUI. `data` should be a table containing the info about your GUI, as formatted as described in GUI Data. 14 | 15 | ### Chains 16 | As a basic method of storing some temporary data that can be shared between multiple GUIs that are opened in a sequence (e.g. pressing a button on one GUI leads to another being opened), GeneralGUI utilises something called a "chain". A chain is first created *before* the first GUI in a sequence is shown, using `GeneralGUI.StartChain(pid)`. This creates a table of data that is fed to and shared between every GUI in the sequence for sharing data, which lasts until the chain is ended, or a new one is created. Even if the new "sequence" you intend to open is just 1-window long, you still need to create a new chain before showing it. 17 | There are a small handful of things that are automatically stored into a chain's data, with the rest being added by your GUI's functions: 18 | - `pid` - The pid of the player for whom the GUI is being shown. This is set when `GeneralGUI.StartChain(pid)` is used, and is how you get the player you want your GUI to target. 19 | - `currentGeneralGuiId` - The ID of the registered GUI that was most recently opened for the player (note that most recently opened could, and generally does, mean the GUI that they currently see). This is set when `GeneralGUI.ShowGUI(pid, id)` is called. It's used internally to reshow the current GUI by `GeneralGUI.ReshowLast`. 20 | - `currentChoiceList` - The choice list (see: *Choice Data*) from the most recently presented choice by a `CustomMessageBox` or `ListBox`. 21 | 22 | If you want your GUIs to store data that lasts beyond the instance of a GUI sequence, you need to store that information elsewhere in your script. 23 | 24 | It's good practice to end a chain when the sequence is over (i.e. when no more GUIs are going to be shown) by using the function `GeneralGUI.EndChain(pid)`. This isn't absolutely necessary, and not doing so will only technically become a problem if another sequence is started without first starting a new chain. 25 | 26 | Currently, only one chain is supported at a time, with chain data for newly opened GUIs overwriting what's currently stored/deleting previous information when told to end. This could change in the future if I want to add a little bit more complexity, but at the moment I feel like just the one is sufficient for most needs. 27 | 28 | The first argument sent to every one of the GUI's functions is the chain data table for the player's current chain. 29 | 30 | ### Choice Data 31 | Both `CustomMessageBox` and `ListBox` GUIs make use of choice data. For `CustomMessageBox`, choices represent the buttons that the player has available. For `ListBox`, choices represent each line on the list. 32 | 33 | There are two pieces of terminology I'm going to use in relation to choices: *choice lists* and *choices/choice data*. A *choice list* is simply an indexed table containing each *choice data* entry, generated by the GUI's `GenerateChoices` function. 34 | 35 | By default, choice data entries contain a few values that are required for GeneralGUI. Additional data can be added during `GenerateChoices` if your script requires more information. 36 | - `index` - The index that the choice appears at in the choice list. To be honest this is pretty pointless and I might just end up removing it as a default requirement. 37 | - `display` - This is a string that will be used when displaying the option to a player. For `CustomMessageBox`es, this is what text will be displayed on the button. For `ListBox`es, this is what the entry says on the list. 38 | - `id` - A string used to later filter the choice during `OnSelectOption`. 39 | - `callback` - This is a function that will be run when the choice is selected. This is entirely optional. If the selected choice doesn't have a callback, `OnSelectOption` is run instead. 40 | 41 | For an example, we're going to assume we're making a `ListBox` that displays every player and actor in a given cell. Inside our `GenerateChoices` function, we'll compile information on all the players and actors in the cell. When making a choice for a player, we'll make `display` be the player's name, the `id` be "player", and add in a custom entry `pid`, which'll be the player's pid. When making a choice for an actor, we'll make `display` be the actor's name, the `id` be "actor", and add in a custom entry `uniqueIndex`, which'll be the actor's uniqueIndex. 42 | Inside our `OnSelectOption` function, we can read the choice data that's provided as the second argument to determine information on the player's selection. If the player chose a player from the list, the `id` will be "player", and we can do whatever logic we want, also utilising the stored `pid`. If the player chose an actor instead, the `id` will be "actor", and we can do something different, utilising the stored `uniqueIndex`. 43 | 44 | ## Script Functions 45 | There are a number of script functions available to use. Read through the file to see them all. Here is a list of the most important ones: 46 | - `RegisterGUI(id, data)` - Use this to register a new GUI for use with GeneralGUI. `id` should be a unique identifier for that GUI, `data` should be a table containing the GUI Data as outlined in the GUI Data section. Obviously use this to register a GUI before it's shown with `ShowGUI`. 47 | - `StartChain(pid)` - Begin a new chain for the player (`pid`). Use *before* `ShowGUI` when beginning a new sequence. See *Chains* section for more information. 48 | - `EndChain(pid)` - End the current chain for the player (`pid`). See *Chains* section for more information. 49 | - `ShowGUI(pid, id)` - Show a registered GUI to the player. `pid` should be the player ID of the player to target. `id` should be the ID of the GUI that was provided when it was registered via `RegisterGUI`. 50 | 51 | ## GUI Data 52 | A GeneralGUI GUI is defined by a table of information that's passed to it during `RegisterGUI`. There are three types of GUI for use in GeneralGUI: `CustomMessageBox`, `InputDialog`, and `ListBox`. The following is a list of every variable that makes up a GUI's data for each type of GUI: 53 | ### InputDialog 54 | - `type` - `string` (required) - Should be `"InputDialog"` 55 | - `isPassword` - `true`/`false` (optional) - If `true` the dialog will use the censored display like a password entry prompt 56 | - `OnInput` - `function` (required) - After all validation has been passed, this function is run. The first argument is the chain data, the second is the `input`. 57 | #### Label 58 | - `GenerateLabel` - `function` (optional) - If present, the function will be run to determine what text to display as the dialog's label (the text above the input box). The first argument given is the chain data. Whatever is returned is used as the label. 59 | - `label` - `string` (optional) - If `GenerateLabel` isn't present, this string will be used for the dialog's label. 60 | 61 | *If both are absent, will default to a blank label.* 62 | #### Note 63 | - `GenerateNote` - `function` (optional) - If present, the function will be run to determine what text to display as the dialog's note (the text below the input box). The first argument given is the chain data. Whatever is returned is used as the label. 64 | - `label` - `string` (optional) - If `GenerateNote` isn't present, this string will be used for the dialog's note. 65 | 66 | *If both are absent, will default to a blank note.* 67 | #### Validation 68 | - `ValidateInput` - `function` (optional) - If present, the function will be run to determine if the provided input is valid. If the input is determined to be invalid, the player will be prompted to enter again. The first argument given is the chain data, the second argument is the string that the player input. The input will be accepted if the function returns `true`, and rejected if it returns `false`. If the function returns a second value when rejecting, that value will be used for the rejection message, otherwise a default message will be used instead. 69 | 70 | ### MessageBox 71 | - `type` - `string` (required) - Should be `"CustomMessageBox"` 72 | #### Label 73 | - `GenerateLabel` - `function` (optional) - If present, the function will be run to determine what text to display as the message's label (the main body of text). The first argument given is the chain data. Whatever is returned is used as the label. 74 | - `label` - `string` (optional) - If `GenerateLabel` isn't present, this string will be used for the message's label. 75 | 76 | *If both are absent, will default to a blank label.* 77 | #### Buttons 78 | - `GenerateChoices` - `function` (optional) - If present, the function will be run to create the GUI's choice list. The first argument given is the chain data. Whatever is returned is used as the choice list (See Choice Data). 79 | 80 | *If absent, the script will generate a choice list with a single entry: `{index = 1, display = "Close", id = "close", callback = GeneralGUI.CloseButton}`* 81 | - `OnSelectOption` - `function` (required if a choice lacks callback) - If a button is selected that lacks a callback function in its data, this function is run instead. The first argument is the chain data, the second is the choice's data. 82 | 83 | ### ListBox 84 | - `type` - `string` (required) - Should be `"ListBox"` 85 | - `requireSelection` - `true`/`false` (optional) - If `true` the GUI won't allow the player to continue without selecting an option from the list. 86 | - `requireSelectionRejectMessage` - `string` (optional) - The message that's displayed if they don't make a selection while `requireSelection` is `true`. If not provided, a default message will be used instead. 87 | #### Label 88 | - `GenerateLabel` - `function` (optional) - If present, the function will be run to determine what text to display as the message's label (the text above the list). The first argument given is the chain data. Whatever is returned is used as the label. 89 | - `label` - `string` (optional) - If `GenerateLabel` isn't present, this string will be used for the message's label. 90 | 91 | *If both are absent, will default to a blank label.* 92 | #### Choices 93 | - `GenerateChoices` - `function` (optional) - If present, the function will be run to create the GUI's choice list. The first argument given is the chain data. Whatever is returned is used as the choice list (See Choice Data). 94 | - `OnSelectOption` - `function` (required if a choice lacks callback) - If a choice is selected that lacks a callback function in its data, this function is run instead. The first argument is the chain data, the second is the choice's data. 95 | 96 | ## Notes 97 | Most of what is here is entirely subject to change. See the example file (`generalGuiExample.lua`) for a passable example of the resource in action :P 98 | -------------------------------------------------------------------------------- /0.7/HotkeysExpanded/readme.md: -------------------------------------------------------------------------------- 1 | # Hotkeys Expanded 2 | Hotkeys Expanded utilises the custom item system to add some new, mostly tes3mp-related functions for use for players via quick keys. These include: equipping sets of items, sending messages (or commands) in chat, playing specific sounds, switching quick key bars, and running script functions with set parameters. 3 | ## Usage 4 | Typing `/hotkey`, `/hotkeys`, `/hex` in chat will open the main menu for this script, provided the user has permission to do so (see *configuration*). From this menu, the player can go through the steps for creating items for any of the special functions that they have permission to create (again, see *configuration*) by following the instructions. 5 | ## Configuration 6 | The following are the more important of the config options: 7 | - **scriptConfig.rank\*** - The `staffRank` required by a player to create items of each function type. The comments in the file explain what each does. Note that this only governs who can *make* what, not whether or not a player can *use* one of these items (there are no checks for that). 8 | - **scriptConfig.commandItemBaseId** - The refId of the item that all HEx items are based off. By default it's `"sc_paper plain"` - a sheet of paper. The script assumes that this will always be a book item, and so should only be changed to an item of that type. 9 | - **scriptConfig.totallyVitalFeatureEnabled** (true/false) - Whether or not the script should add some easter egg text when it creates an item. When everything is fully functioning, it's unlikely players will ever see these messages, and so all this ultimately leads to is taking up some unnecessary extra memory space. 10 | ## Language Configuration 11 | Almost every piece of text that's presented to the player (with the exception of easter egg text because ???) can easily be changed by configuring the `lang` section. Keep the keys as they are, and edit their strings to read anything you want. Note that words beginning with `%` are special wildcards (? I think that's the term) and shouldn't be translated (i.e. `%name` should stay as `%name`) - the script will automatically replace any instance of them with special text. 12 | 13 | For example: `equipOutfitItemName` determines what name to give to a HEx item that's used for equipping an outfit. In this case, `%name` is used as a placeholder for the name of the outfit. So were a player to create a HEx item for an outfit they named `armor`, `[HEx] Equip Outfit: %name` would become `[HEx] Equip Outfit: armor` 14 | ## Script Methods 15 | There are a lot of Methods exposed for other scripts to use if they want to, but the 2 main ones specifically designed to be utilised by other scripts are: 16 | - **HotkeysExpanded.RegisterScriptFunction(id, func)** - Use this to register a function to be utilised via this script's *Run Function* feature. `id` should be a unique identifier for your function - it's what you need to enter into the run function dialog, as well what's needed if you want to unregister the function. `func` is the function that'll be run when a *Run Function* item is used. When run, it'll be fed the player's arg string as its first argument. 17 | - **HotkeysExpanded.UnregisterScriptFunction(id)** - Use to unregister the function of the given `id`, should that be needed. 18 | 19 | ## Known Issues 20 | - As of the time of writing, there's a bug in v0.7-prerelease which prevents this script's items from being used via quick keys. They can still be used regularly, however. 21 | - Two items with the same name will combine in the inventory, potentially losing a script item (only possible/a problem for Equip Outfit items). 22 | - When switching to a new quick key bar using a Switch Bar item, the player will see a phantom quick key in their quick key bar. This isn't actually saved in their quick key bar, and will disappear on reconnect/when overwritten. 23 | - Not an issue per se, but at present there isn't any way to delete created HEx items. 24 | -------------------------------------------------------------------------------- /0.7/ServerWarp/README.md: -------------------------------------------------------------------------------- 1 | # ServerWarp 2 | 3 | Adds a `/warp` series of commands for ... warping. 4 | 5 | ## Usage 6 | 7 | Command list: 8 | 9 | * `/warplist` 10 | * Prints a list of all public warps and your own private warps into chat 11 | * `/warp ` 12 | * Requires permission: useWarpRank 13 | * Warp yourself to a warp with the provided name. It first checks your personal warp list and if it can't find a warp by that name it then checks the public warp list. You can't use this command if your warp privilege has been disabled. 14 | * `/warpset ` 15 | * Requires permission: setWarpRank 16 | * Records your current position as a personal warp point with the provided name 17 | * `/warpsetpublic ` 18 | * Requires permission: setPublicWarpRank 19 | * Records your current position as a public warp point with the provided name 20 | * `/warpremove ` 21 | * Requires permission: setWarpRank 22 | * Removes the named warp from your personal warp list 23 | * `/warpremovepublic ` 24 | * Requires permission: removePublicWarpRank 25 | * Removes the named warp from the public warp list 26 | * `/warpforce ` 27 | * Requires permission: forcePlayerRank 28 | * Forcibly teleports the player with the provided id to a public warp with the given name 29 | * `/warpjail ` 30 | * Requires permission: forcePlayerRank AND forceJailPlayerRank 31 | * As with /forcewarp, but also disables the player's warp privileges 32 | * `/warpallow <0/1 to disable/enable>` 33 | * Requires permission: setAllowWarp 34 | * Sets the targeted player's warp privileges. Set to 0 to disable them from using the /warp command, set to 1 to enable them again. 35 | * Example usage: 36 | 37 | /warpforce 1 the forum 38 | -------------------------------------------------------------------------------- /0.7/ServerWarp/ServerWarp.lua: -------------------------------------------------------------------------------- 1 | -- ServerWarp - Release 3 - For tes3mp 0.7-alpha 2 | 3 | local ServerWarp = {} 4 | 5 | --[[ =Notes= 6 | Warpdata structure: 7 | cell 8 | posX 9 | posY 10 | posZ 11 | rotX 12 | rotZ 13 | ]] 14 | 15 | local config = {} 16 | --The minimum rank required to perform any of the actions. 0 is a regular player, 1 is a moderator and 2 is an admin. 17 | config.setWarpRank = 1 --Also used to determine if they can remove their own warps 18 | config.setPublicWarpRank = 2 19 | config.useWarpRank = 0 --Don't differentiate between Public and Private warps for simplicity 20 | config.removePublicWarpRank = 2 21 | config.forcePlayerRank = 1 22 | config.forceJailPlayerRank = 1 --Players also require the permissions to forcePlayer to use this command. 23 | config.setAllowWarp = 1 24 | 25 | ServerWarp.OnSetWarpCommand = function(pid, args) 26 | local command = args[1] 27 | local isPublic 28 | local warpName = tableHelper.concatenateFromIndex(args, 2) 29 | 30 | if warpName == "" then 31 | tes3mp.SendMessage(pid, "Please provide a warp name.\n", false) 32 | return 33 | end 34 | 35 | if command == "warpsetpublic" then 36 | isPublic = true 37 | else 38 | isPublic = false 39 | end 40 | 41 | local rank = Players[pid].data.settings.staffRank 42 | 43 | --Check player has the correct rank 44 | if isPublic and (rank < config.setPublicWarpRank) then 45 | tes3mp.SendMessage(pid, "Your rank is too low to set Public Warps.\n", false) 46 | return false 47 | elseif rank < config.setWarpRank then 48 | tes3mp.SendMessage(pid, "Your rank is too low to set Warps.\n", false) 49 | return false 50 | end 51 | 52 | local newWarp = {} 53 | 54 | newWarp.cell = tes3mp.GetCell(pid) 55 | newWarp.posX = tes3mp.GetPosX(pid) 56 | newWarp.posY = tes3mp.GetPosY(pid) 57 | newWarp.posZ = tes3mp.GetPosZ(pid) 58 | newWarp.rotX = tes3mp.GetRotX(pid) 59 | newWarp.rotZ = tes3mp.GetRotZ(pid) 60 | 61 | --Note: Will overwrite existing warps of the same name 62 | if isPublic then 63 | ServerWarp.AddPublicWarp(warpName, newWarp) 64 | else 65 | ServerWarp.AddPrivateWarp(pid, warpName, newWarp) 66 | end 67 | 68 | tes3mp.SendMessage(pid, "Warp added.\n", false) 69 | return true 70 | end 71 | 72 | ServerWarp.OnRemoveWarpCommand = function(pid, args) 73 | local command = args[1] 74 | local isPublic 75 | local warpName = tableHelper.concatenateFromIndex(args, 2) 76 | 77 | if warpName == "" then 78 | tes3mp.SendMessage(pid, "Please provide the target warp name.\n", false) 79 | return 80 | end 81 | 82 | if command == "warpremovepublic" then 83 | isPublic = true 84 | else 85 | isPublic = false 86 | end 87 | 88 | local rank = Players[pid].data.settings.staffRank 89 | --Check player permissions 90 | if isPublic and (rank < config.removePublicWarpRank) then 91 | tes3mp.SendMessage(pid, "Your rank is too low to remove Public Warps.\n", false) 92 | return false 93 | --Doesn't have a unique config entry - if the player can set private warps, they're allowed to delete them 94 | elseif rank < config.setWarpRank then 95 | tes3mp.SendMessage(pid, "Your rank is too low to remove Warps.\n", false) 96 | return false 97 | end 98 | 99 | --Find the Warp to remove 100 | local list 101 | 102 | if isPublic then 103 | list = ServerWarp.GetPublicWarps() 104 | else 105 | list = ServerWarp.GetPrivateWarps(pid) 106 | end 107 | 108 | --If the Warp exists, remove it, otherwise error 109 | --(This should probably be in its own function...) 110 | local warpName = string.lower(warpName) -- Warps are stored as lowercase 111 | if list[warpName] ~= nil then 112 | list[warpName] = nil 113 | if isPublic then 114 | WorldInstance:Save() 115 | tes3mp.SendMessage(pid, "Removed Public Warp by the name '" .. warpName .. "'.\n", false) 116 | else 117 | Players[pid]:Save() 118 | tes3mp.SendMessage(pid, "Removed Warp by the name '" .. warpName .. "'.\n", false) 119 | end 120 | return true 121 | else 122 | tes3mp.SendMessage(pid, "Couldn't find a Warp by the name '" .. warpName .. "'.\n", false) 123 | return false 124 | end 125 | end 126 | 127 | ServerWarp.OnWarpCommand = function(pid, args) 128 | local warpName = tableHelper.concatenateFromIndex(args, 2) 129 | 130 | local rank = Players[pid].data.settings.staffRank 131 | 132 | --Check the player can warp 133 | if ServerWarp.isWarpEnabled(pid) == false then 134 | tes3mp.SendMessage(pid, "You can't Warp at this time.\n", false) 135 | return false 136 | --Check their rank 137 | elseif rank < config.useWarpRank then 138 | tes3mp.SendMessage(pid, "Your rank is too low to use Warps.\n", false) 139 | return false 140 | end 141 | 142 | local foundWarp = ServerWarp.FindWarp(warpName, pid, false) --Prioritises private warps over public ones 143 | 144 | if foundWarp then 145 | ServerWarp.WarpPlayer(pid, foundWarp) 146 | tes3mp.SendMessage(pid, "You have Warped to " .. warpName ..".\n", false) 147 | return true 148 | else 149 | tes3mp.SendMessage(pid, "Couldn't find a Warp with that name.\n", false) 150 | return false 151 | end 152 | end 153 | 154 | ServerWarp.OnWarpListCommand = function(pid) 155 | local pubWarps = ServerWarp.GetPublicWarps() 156 | local privWarps = ServerWarp.GetPrivateWarps(pid) 157 | 158 | --Public warps list 159 | local message = "Public Warps:\n" 160 | for k, _ in pairs(pubWarps) do 161 | message = message .. "> " .. k .. "\n" 162 | end 163 | --Private warps list 164 | message = message .. "Your Warps:\n" 165 | for k, _ in pairs(privWarps) do 166 | message = message .. "> " .. k .. "\n" 167 | end 168 | 169 | tes3mp.SendMessage(pid, message, false) 170 | end 171 | 172 | ServerWarp.ForceWarp = function(pid, targetId, warpName, cantWarp) 173 | if cantWarp then 174 | ServerWarp.SetCanWarp(targetId, 0) 175 | end 176 | 177 | local foundWarp = ServerWarp.FindWarp(warpName, nil, true) 178 | 179 | ServerWarp.WarpPlayer(targetId, foundWarp) 180 | end 181 | 182 | ServerWarp.OnForcePlayerCommand = function (pid, args) 183 | local targetId = args[2] or nil 184 | local warpName = tableHelper.concatenateFromIndex(args, 3) 185 | 186 | if targetId == nil or warpName == "" then 187 | tes3mp.SendMessage(pid, "Please provide the target id and warp name.\n", false) 188 | return 189 | end 190 | 191 | local rank = Players[pid].data.settings.staffRank 192 | 193 | if rank < config.forcePlayerRank then 194 | tes3mp.SendMessage(pid, "Your rank is too low to force Warp players.\n", false) 195 | return false 196 | end 197 | 198 | local foundWarp = ServerWarp.FindWarp(warpName, nil, true) 199 | 200 | if not foundWarp then 201 | tes3mp.SendMessage(pid, "Couldn't find the Warp.\n", false) 202 | return false 203 | end 204 | 205 | ServerWarp.ForceWarp(pid, targetId, warpName, false) 206 | tes3mp.SendMessage(pid, "Warped " .. --[[Players[targetId].name]] "player" .. " to " .. warpName .. ".\n", false) 207 | tes3mp.SendMessage(targetId, "You were warped to " .. warpName .. " by " .. Players[pid].name .. ".\n", false) 208 | 209 | return true 210 | end 211 | 212 | --Basically uses existing commands to teleport a player to a specific warp and disable their ability to warp. 213 | ServerWarp.OnJailPlayerCommand = function(pid, args) 214 | local targetId = args[2] or nil 215 | local warpName = tableHelper.concatenateFromIndex(args, 3) 216 | 217 | if targetId == nil or warpName == "" then 218 | tes3mp.SendMessage(pid, "Please provide the target id and warp name.\n", false) 219 | return 220 | end 221 | 222 | local rank = Players[pid].data.settings.staffRank 223 | 224 | if rank < config.forceJailPlayerRank then 225 | tes3mp.SendMessage(pid, "Your rank is too low to jail players.\n", false) 226 | return false 227 | end 228 | 229 | local foundWarp = ServerWarp.FindWarp(warpName, nil, true) 230 | 231 | if not foundWarp then 232 | tes3mp.SendMessage(pid, "Couldn't find the Warp.\n", false) 233 | return false 234 | end 235 | 236 | ServerWarp.ForceWarp(pid, targetId, warpName, true) 237 | tes3mp.SendMessage(pid, "Warped " .. --[[Players[targetId].name]] "player" .. " to " .. warpName .. ".\n", false) 238 | tes3mp.SendMessage(targetId, "You were warped to " .. warpName .. " by " .. Players[pid].name .. ".\n", false) 239 | 240 | return true 241 | end 242 | 243 | ServerWarp.OnSetCanWarpCommand = function(pid, args) 244 | local targetId = args[2] or nil 245 | local value = args[3] or nil 246 | 247 | if targetId == nil or value == nil then 248 | tes3mp.SendMessage(pid, "Please provide a valid player id and a value to set it to (0 or 1).\n", false) 249 | return false 250 | end 251 | 252 | local rank = Players[pid].data.settings.staffRank 253 | 254 | if rank < config.setAllowWarp then 255 | tes3mp.SendMessage(pid, "Your rank is too low to change a player's warp privileges.\n", false) 256 | return false 257 | end 258 | 259 | ServerWarp.SetCanWarp(targetId, value) 260 | end 261 | 262 | ServerWarp.SetCanWarp = function(pid, val) 263 | --Make sure the arguments are valid 264 | local pid = tonumber(pid) 265 | local val = tonumber(val) 266 | --Use 0 to disable, 1 to enable 267 | Players[pid].data.customVariables.canServerWarp = val 268 | Players[pid]:Save() 269 | end 270 | 271 | ServerWarp.isWarpEnabled = function(pid) 272 | if tonumber(Players[pid].data.customVariables.canServerWarp) == 0 then 273 | return false 274 | else 275 | return true 276 | end 277 | end 278 | 279 | ServerWarp.GetPublicWarps = function() 280 | --If there are no public warps, create the table 281 | if WorldInstance.data.customVariables.serverWarp == nil then 282 | WorldInstance.data.customVariables.serverWarp = {} 283 | WorldInstance:Save() 284 | end 285 | 286 | return WorldInstance.data.customVariables.serverWarp 287 | end 288 | 289 | ServerWarp.GetPrivateWarps = function(pid) 290 | --If there are no private warps, create the table 291 | if Players[pid].data.customVariables.serverWarp == nil then 292 | Players[pid].data.customVariables.serverWarp = {} 293 | Players[pid]:Save() 294 | end 295 | 296 | return Players[pid].data.customVariables.serverWarp 297 | end 298 | 299 | ServerWarp.AddPublicWarp = function(warpName, data) 300 | local warps = ServerWarp.GetPublicWarps() 301 | warps[string.lower(warpName)] = data 302 | WorldInstance:Save() 303 | end 304 | 305 | ServerWarp.AddPrivateWarp = function(pid, warpName, data) 306 | if not warpName then 307 | return false 308 | end 309 | local warps = ServerWarp.GetPrivateWarps(pid) 310 | warps[string.lower(warpName)] = data 311 | Players[pid]:Save() 312 | end 313 | 314 | ServerWarp.FindWarp = function(warpName, pid, prioritisePublic) 315 | local pubWarps = ServerWarp.GetPublicWarps() 316 | local privWarps 317 | 318 | if not warpName then 319 | return false 320 | end 321 | 322 | local warpName = string.lower(warpName) 323 | 324 | local pubCheck = ServerWarp.SearchWarps(pubWarps, warpName) 325 | local privCheck 326 | 327 | if pid then 328 | privWarps = ServerWarp.GetPrivateWarps(pid) 329 | privCheck = ServerWarp.SearchWarps(privWarps, warpName) 330 | end 331 | 332 | if prioritisePublic then 333 | return pubCheck or privCheck or false 334 | else 335 | return privCheck or pubCheck or false 336 | end 337 | end 338 | 339 | ServerWarp.WarpPlayer = function(pid, warpData) 340 | tes3mp.SetCell(pid, warpData.cell) 341 | tes3mp.SendCell(pid) 342 | 343 | tes3mp.SetPos(pid, warpData.posX, warpData.posY, warpData.posZ) 344 | tes3mp.SetRot(pid, warpData.rotX, warpData.rotZ) 345 | tes3mp.SendPos(pid) 346 | end 347 | 348 | ServerWarp.SearchWarps = function (warps, warpName) 349 | --should never get to here without first being turned into lowercase, but we'll do it here again just in case 350 | local warpName = string.lower(warpName) 351 | for k,v in pairs(warps) do 352 | if k == warpName then 353 | return v 354 | end 355 | end 356 | 357 | return false 358 | end 359 | 360 | customCommandHooks.registerCommand("warp", ServerWarp.OnWarpCommand) 361 | customCommandHooks.registerCommand("warpallow", ServerWarp.OnSetCanWarpCommand) 362 | customCommandHooks.registerCommand("warpforce", ServerWarp.OnForcePlayerCommand) 363 | customCommandHooks.registerCommand("warpjail", ServerWarp.OnJailPlayerCommand) 364 | customCommandHooks.registerCommand("warplist", ServerWarp.OnWarpListCommand) 365 | customCommandHooks.registerCommand("warpset", ServerWarp.OnSetWarpCommand) 366 | customCommandHooks.registerCommand("warpsetpublic", ServerWarp.OnSetWarpCommand) 367 | customCommandHooks.registerCommand("warpremove", ServerWarp.OnRemoveWarpCommand) 368 | customCommandHooks.registerCommand("warpremovepublic", ServerWarp.OnRemoveWarpCommand) 369 | 370 | return ServerWarp 371 | -------------------------------------------------------------------------------- /0.7/decorateHelp.lua: -------------------------------------------------------------------------------- 1 | -- decorateHelp - Release 4 - For tes3mp v0.7.0-alpha 2 | -- Alter positions of items using a GUI 3 | 4 | --[[ INSTALLATION: 5 | 1) Save this file as "decorateHelp.lua" in server/scripts/custom 6 | 2) Add [ decorateHelp = require("custom.decorateHelp") ] to customScripts.lua 7 | ]] 8 | 9 | ------ 10 | local config = {} 11 | 12 | config.MainId = 31360 13 | config.PromptId = 31361 14 | config.ScaleMin = 0.5 15 | config.ScaleMax = 2.0 16 | ------ 17 | 18 | Methods = {} 19 | 20 | tableHelper = require("tableHelper") 21 | 22 | -- 23 | local playerSelectedObject = {} 24 | local playerCurrentMode = {} 25 | 26 | --Returns the object's data from a loaded cell. Doesn't need to load the cell because this assumes it'll always be called in a cell that's loaded. 27 | local function getObject(refIndex, cell) 28 | if refIndex == nil then 29 | return false 30 | end 31 | 32 | if LoadedCells[cell]:ContainsObject(refIndex) then 33 | return LoadedCells[cell].data.objectData[refIndex] 34 | else 35 | return false 36 | end 37 | end 38 | 39 | local function resendPlaceToAll(refIndex, cell) 40 | local object = getObject(refIndex, cell) 41 | 42 | if not object then 43 | return false 44 | end 45 | 46 | local refId = object.refId 47 | local count = object.count or 1 48 | local charge = object.charge or -1 49 | local posX, posY, posZ = object.location.posX, object.location.posY, object.location.posZ 50 | local rotX, rotY, rotZ = object.location.rotX, object.location.rotY, object.location.rotZ 51 | local refIndex = refIndex 52 | local scale = object.scale or 1 53 | 54 | local inventory = object.inventory or nil 55 | 56 | local splitIndex = refIndex:split("-") 57 | 58 | for pid, pdata in pairs(Players) do 59 | if Players[pid]:IsLoggedIn() then 60 | --First, delete the original 61 | tes3mp.InitializeEvent(pid) 62 | tes3mp.SetEventCell(cell) 63 | tes3mp.SetObjectRefNumIndex(0) 64 | tes3mp.SetObjectMpNum(splitIndex[2]) 65 | tes3mp.AddWorldObject() --? 66 | tes3mp.SendObjectDelete() 67 | 68 | --Now remake it 69 | tes3mp.InitializeEvent(pid) 70 | tes3mp.SetEventCell(cell) 71 | tes3mp.SetObjectRefId(refId) 72 | tes3mp.SetObjectCount(count) 73 | tes3mp.SetObjectCharge(charge) 74 | tes3mp.SetObjectPosition(posX, posY, posZ) 75 | tes3mp.SetObjectRotation(rotX, rotY, rotZ) 76 | tes3mp.SetObjectRefNumIndex(0) 77 | tes3mp.SetObjectMpNum(splitIndex[2]) 78 | tes3mp.SetObjectScale(scale) 79 | if inventory then 80 | for itemIndex, item in pairs(inventory) do 81 | tes3mp.SetContainerItemRefId(item.refId) 82 | tes3mp.SetContainerItemCount(item.count) 83 | tes3mp.SetContainerItemCharge(item.charge) 84 | 85 | tes3mp.AddContainerItem() 86 | end 87 | end 88 | 89 | tes3mp.AddWorldObject() 90 | tes3mp.SendObjectPlace() 91 | tes3mp.SendObjectScale() 92 | if inventory then 93 | tes3mp.SendContainer() 94 | end 95 | end 96 | end 97 | 98 | -- Make sure to save a scale packet if this object has a non-default scale. 99 | if scale ~= 1 then 100 | tableHelper.insertValueIfMissing(LoadedCells[cell].data.packets.scale, refIndex) 101 | end 102 | LoadedCells[cell]:QuicksaveToDrive() --Not needed, but it's nice to do anyways 103 | end 104 | 105 | 106 | local function showPromptGUI(pid) 107 | local message = "[" .. playerCurrentMode[tes3mp.GetName(pid)] .. "] - Enter a number." 108 | local pname = tes3mp.GetName(pid) 109 | local cell = tes3mp.GetCell(pid) 110 | 111 | if playerCurrentMode[pname] == "Fine Tune Scale" then 112 | local object = getObject(playerSelectedObject[pname], cell) 113 | local scale = object.scale or 1 114 | tes3mp.InputDialog(pid, config.PromptId, message, "Current scale: " .. scale .. "\nMinimum value: " .. config.ScaleMin .. "\nMaximum value: " .. config.ScaleMax) 115 | else 116 | tes3mp.InputDialog(pid, config.PromptId, message, "Enter a number to add/subtract.\nPositives increase.\nNegatives decrease.") 117 | end 118 | end 119 | 120 | local function onEnterPrompt(pid, data) 121 | local cell = tes3mp.GetCell(pid) 122 | local pname = tes3mp.GetName(pid) 123 | local mode = playerCurrentMode[pname] 124 | local data = tonumber(data) or 0 125 | 126 | local object = getObject(playerSelectedObject[pname], cell) 127 | 128 | if not object then 129 | --The object no longer exists, so we should bail out now 130 | return false 131 | end 132 | 133 | local scale = object.scale or 1 134 | 135 | if mode == "Rotate X" then 136 | local curDegrees = math.deg(object.location.rotX) 137 | local newDegrees = (curDegrees + data) % 360 138 | object.location.rotX = math.rad(newDegrees) 139 | elseif mode == "Rotate Y" then 140 | local curDegrees = math.deg(object.location.rotY) 141 | local newDegrees = (curDegrees + data) % 360 142 | object.location.rotY = math.rad(newDegrees) 143 | elseif mode == "Rotate Z" then 144 | local curDegrees = math.deg(object.location.rotZ) 145 | local newDegrees = (curDegrees + data) % 360 146 | object.location.rotZ = math.rad(newDegrees) 147 | elseif mode == "Fine Tune North" then 148 | object.location.posY = object.location.posY + data 149 | elseif mode == "Fine Tune East" then 150 | object.location.posX = object.location.posX + data 151 | elseif mode == "Fine Tune Height" then 152 | object.location.posZ = object.location.posZ + data 153 | elseif mode == "Raise" then 154 | object.location.posZ = object.location.posZ + 10 155 | elseif mode == "Lower" then 156 | object.location.posZ = object.location.posZ - 10 157 | elseif mode == "Move East" then 158 | object.location.posX = object.location.posX + 10 159 | elseif mode == "Move West" then 160 | object.location.posX = object.location.posX - 10 161 | elseif mode == "Move North" then 162 | object.location.posY = object.location.posY + 10 163 | elseif mode == "Move South" then 164 | object.location.posY = object.location.posY - 10 165 | elseif mode == "Scale Up" then 166 | if scale + 0.1 <= config.ScaleMax then 167 | object.scale = scale + 0.1 168 | end 169 | elseif mode == "Scale Down" then 170 | if scale - 0.1 >= config.ScaleMin then 171 | object.scale = scale - 0.1 172 | end 173 | elseif mode == "Fine Tune Scale" then 174 | if data <= config.ScaleMax and data >= config.ScaleMin then 175 | object.scale = data 176 | end 177 | elseif mode == "return" then 178 | object.location.posY = object.location.posY 179 | return 180 | end 181 | 182 | resendPlaceToAll(playerSelectedObject[pname], cell) 183 | end 184 | 185 | local function showMainGUI(pid) 186 | --Determine if the player has an item 187 | local currentItem = "None" --default 188 | local selected = playerSelectedObject[tes3mp.GetName(pid)] 189 | local object = getObject(selected, tes3mp.GetCell(pid)) 190 | 191 | if selected and object then --If they have an entry and it isn't gone 192 | currentItem = object.refId .. " (" .. selected .. ")" 193 | end 194 | 195 | local message = "Select an option. Your current item is: " .. currentItem 196 | tes3mp.CustomMessageBox(pid, config.MainId, message, "Select Furniture;Fine Tune North;Fine Tune East;Fine Tune Height;Rotate X;Rotate Y;Rotate Z;Raise;Lower;Move East;Move West;Move North;Move South;Scale Up;Scale Down;Fine Tune Scale;Exit") 197 | end 198 | 199 | local function setSelectedObject(pid, refIndex) 200 | playerSelectedObject[tes3mp.GetName(pid)] = refIndex 201 | end 202 | 203 | Methods.SetSelectedObject = function(pid, refIndex) 204 | setSelectedObject(pid, refIndex) 205 | end 206 | 207 | Methods.OnObjectPlace = function(pid, cellDescription) 208 | --Get the last event, which should hopefully be the place packet 209 | tes3mp.ReadLastEvent() 210 | 211 | --Get the refIndex of the first item in the object place packet (in theory, there should only by one) 212 | local refIndex = tes3mp.GetObjectRefNumIndex(0) .. "-" .. tes3mp.GetObjectMpNum(0) 213 | 214 | --Record that item as the last one the player interacted with in this cell 215 | setSelectedObject(pid, refIndex) 216 | end 217 | 218 | Methods.OnGUIAction = function(pid, idGui, data) 219 | local pname = tes3mp.GetName(pid) 220 | 221 | if idGui == config.MainId then 222 | if tonumber(data) == 0 then --View Furniture Emporium 223 | playerCurrentMode[pname] = "Select Furniture" 224 | kanaFurniture.OnCommand(pid) 225 | return true 226 | elseif tonumber(data) == 1 then --Move North 227 | playerCurrentMode[pname] = "Fine Tune North" 228 | showPromptGUI(pid) 229 | return true 230 | elseif tonumber(data) == 2 then --Move East 231 | playerCurrentMode[pname] = "Fine Tune East" 232 | showPromptGUI(pid) 233 | return true 234 | elseif tonumber(data) == 3 then --Move Up 235 | playerCurrentMode[pname] = "Fine Tune Height" 236 | showPromptGUI(pid) 237 | return true 238 | elseif tonumber(data) == 4 then --Rotate X 239 | playerCurrentMode[pname] = "Rotate X" 240 | showPromptGUI(pid) 241 | return true 242 | elseif tonumber(data) == 5 then --Rotate Y 243 | playerCurrentMode[pname] = "Rotate Y" 244 | showPromptGUI(pid) 245 | return true 246 | elseif tonumber(data) == 6 then --Rotate Z 247 | playerCurrentMode[pname] = "Rotate Z" 248 | showPromptGUI(pid) 249 | return true 250 | elseif tonumber(data) == 7 then --,Ascend 251 | playerCurrentMode[pname] = "Raise" 252 | onEnterPrompt(pid, 0) 253 | return true, showMainGUI(pid) 254 | elseif tonumber(data) == 8 then --Descend 255 | playerCurrentMode[pname] = "Lower" 256 | onEnterPrompt(pid, 0) 257 | return true, showMainGUI(pid) 258 | elseif tonumber(data) == 9 then --East 259 | playerCurrentMode[pname] = "Move East" 260 | onEnterPrompt(pid, 0) 261 | return true, showMainGUI(pid) 262 | elseif tonumber(data) == 10 then --West 263 | playerCurrentMode[pname] = "Move West" 264 | onEnterPrompt(pid, 0) 265 | return true, showMainGUI(pid) 266 | elseif tonumber(data) == 11 then --North 267 | playerCurrentMode[pname] = "Move North" 268 | onEnterPrompt(pid, 0) 269 | return true, showMainGUI(pid) 270 | elseif tonumber(data) == 12 then --South 271 | playerCurrentMode[pname] = "Move South" 272 | onEnterPrompt(pid, 0) 273 | return true, showMainGUI(pid) 274 | elseif tonumber(data) == 13 then -- Scale up by 0.1 275 | playerCurrentMode[pname] = "Scale Up" 276 | onEnterPrompt(pid, 0) 277 | return true, showMainGUI(pid) 278 | elseif tonumber(data) == 14 then -- Scale down by 0.1 279 | playerCurrentMode[pname] = "Scale Down" 280 | onEnterPrompt(pid, 0) 281 | return true, showMainGUI(pid) 282 | elseif tonumber(data) == 15 then -- Scale 283 | playerCurrentMode[pname] = "Fine Tune Scale" 284 | showPromptGUI(pid) 285 | return true 286 | elseif tonumber(data) == 16 then --Close 287 | --Do nothing 288 | return true 289 | end 290 | elseif idGui == config.PromptId then 291 | if data ~= nil and data ~= "" and tonumber(data) then 292 | onEnterPrompt(pid, data) 293 | end 294 | 295 | playerCurrentMode[tes3mp.GetName(pid)] = nil 296 | return true, showMainGUI(pid) 297 | end 298 | end 299 | 300 | Methods.OnPlayerCellChange = function(pid) 301 | playerSelectedObject[tes3mp.GetName(pid)] = nil 302 | end 303 | 304 | Methods.OnCommand = function(pid) 305 | showMainGUI(pid) 306 | end 307 | 308 | customCommandHooks.registerCommand("decorator", function(pid, cmd) decorateHelp.OnCommand(pid) end) 309 | customCommandHooks.registerCommand("decorate", function(pid, cmd) decorateHelp.OnCommand(pid) end) 310 | customCommandHooks.registerCommand("dh", function(pid, cmd) decorateHelp.OnCommand(pid) end) 311 | 312 | customEventHooks.registerHandler("OnGUIAction", function(eventStatus, pid, idGui, data) 313 | decorateHelp.OnGUIAction(pid, idGui, data) 314 | end) 315 | 316 | customEventHooks.registerHandler("OnObjectPlace", function(eventStatus, pid, cellDescription, objects) 317 | decorateHelp.OnObjectPlace(pid, cellDescription) 318 | end) 319 | 320 | customEventHooks.registerHandler("OnPlayerCellChange", function(eventStatus, pid, previousCellDescription, currentCellDescription) 321 | decorateHelp.OnPlayerCellChange(pid) 322 | end) 323 | 324 | return Methods 325 | -------------------------------------------------------------------------------- /0.7/flatModifiers.lua: -------------------------------------------------------------------------------- 1 | -- flatModifiers (Advanced) - Release 3 - For tes3mp v0.7-alpha Requires classInfo. 2 | 3 | --[[ INSTALLATION 4 | 1) Save this file as "flatModifiers.lua" in server/scripts/custom 5 | 2) Add [ flatModifiers = require("custom.flatModifiers") ] to customScripts.lua 6 | ]] 7 | 8 | --[[ NOTE 9 | The values this script use are for fake level ups towards attributes, rather than the set multiplier to provide the attribute. These level ups translate as the following 10 | 1-4 gives 2x, 5-7 gives 3x, 8-9 gives 4x, 10+ gives 5x 11 | ]] 12 | 13 | local Methods = {} 14 | 15 | classInfo = require("classInfo") 16 | 17 | local config = {} 18 | 19 | config.mode = "basic" -- "basic" or "class" 20 | --Basic mode sets all attribute increases to a flat value (determined by config.basicAttributeIncreases) 21 | --Class mode has attribute increases tailored to the character's class (see class mode config options section for config options) 22 | 23 | --globally used config options 24 | config.includeLuck = false --Whether to include Luck in the calculations. By default, Morrowind doesn't allow bonuses to Luck. 25 | 26 | --basic mode config options 27 | config.basicAttributeIncreases = 6 -- Number of skill increases towards the stats that the script should fake. 28 | 29 | --class mode config options 30 | config.classBase = 3 -- How many skill advances every attribute has for its base 31 | config.classMajorSkillBonus = 1.5 -- How many skill advances get added to an attribute per major skill governed by it 32 | config.classMinorSkillBonus = 1 -- How many skill advances get added to an attribute per minor skill governed by it 33 | config.classAttributeBonus = 3 -- How many skill advances get added to an attribute which is one of the class' major attributes 34 | 35 | local function basicMode(pid) 36 | for i = 0, 7 do 37 | if i ~= 7 or config.includeLuck then --Avoid giving Luck (7) any bonus, unless configured to 38 | Players[pid].data.attributes[tes3mp.GetAttributeName(i)].skillIncrease = config.basicAttributeIncreases 39 | end 40 | end 41 | Players[pid]:LoadAttributes() 42 | end 43 | 44 | local function classMode(pid) 45 | local pClass = classInfo.GetPlayerClassData(pid) 46 | 47 | local changes = {} 48 | 49 | --Major attributes 50 | for k, v in pairs(pClass.majorAttributes) do 51 | changes[v] = (changes[v] or 0) + config.classAttributeBonus 52 | end 53 | 54 | --Major skills 55 | for k, v in pairs(pClass.majorSkills) do 56 | local governed = classInfo.GetGovernedAttribute(v) 57 | changes[governed] = (changes[governed] or 0) + config.classMajorSkillBonus 58 | end 59 | 60 | --Minor skills 61 | for k, v in pairs(pClass.minorSkills) do 62 | local governed = classInfo.GetGovernedAttribute(v) 63 | changes[governed] = (changes[governed] or 0) + config.classMinorSkillBonus 64 | end 65 | 66 | --Finally make the changes 67 | for i = 0, 7 do 68 | if i ~= 7 or config.includeLuck then --Avoid giving Luck (7) any bonus, unless configured to 69 | local amount = config.classBase + (changes[i] or 0) 70 | amount = math.min(math.floor(amount), 10) 71 | Players[pid].data.attributes[tes3mp.GetAttributeName(i)].skillIncrease = amount 72 | end 73 | end 74 | 75 | Players[pid]:LoadAttributes() 76 | tes3mp.SendSkills(pid) --Required until 0.7-prerelease bug is fixed 77 | end 78 | 79 | local function doTheThing(eventStatus, pid) 80 | if eventStatus.validCustomHandlers then 81 | if Players[pid] ~= nil and Players[pid]:IsLoggedIn() then 82 | if config.mode == "basic" then 83 | basicMode(pid) 84 | else 85 | classMode(pid) 86 | end 87 | end 88 | end 89 | end 90 | 91 | customEventHooks.registerHandler("OnPlayerLevel", doTheThing) 92 | 93 | return Methods 94 | -------------------------------------------------------------------------------- /0.7/kanaBank/readme.md: -------------------------------------------------------------------------------- 1 | # kanaBank 2 | Provides access to personal storage for players to utilise via command, or by activating pre-designating "bankers". 3 | 4 | *Currently written for a version of 0.7-alpha* 5 | 6 | ## Usage 7 | Banks provide a personal storage for every player, which can be accessed in a variety of ways depending on configuration. Those that meet the `useBankCommandRank` rank requirement can use the `/bank` to open their bank storage, and those that meet the `openOtherPlayersBankRank` rank can use `/bank PlayerName` to open the banks of others... Otherwise (or in addition to), players that meet the rank of `useBankerRank` can access their bank storage by activating a banker object (this is useful, if you want to restrict where players will be able to access the storage). What object counts as a banker object is defined by the server owner via configuration, or outside scripts utilising the provided methods. 8 | 9 | ## Configuration 10 | Configuration is done from within the file itself, by altering the `scriptConfig` values. 11 | ### Rank Options 12 | The required `staffRank` to use each of the script's features 13 | - `useBankerRank` - The rank required for a player to open their bank by activating a banker. 14 | - `useBankCommandRank` - The rank required for a player to open their bank via the `/bank` command. 15 | - `openOtherPlayersBankRank` - The rank required for a player to open another player's bank via the `/bank PlayerName` command. 16 | ### Bankers 17 | Define what objects act as bankers. 18 | - `bankerRefIds` - Objects with `refId`s inserted here will be treated as bankers. Note that this applies to *all* instances that have that `refId` - if you want to apply it to one specific instance, use the next option. 19 | - `bankerUniqueIndexes` - Objects with their `uniqueIndex` inserted here will be treated as bankers. 20 | 21 | *Note: Objects defined as bankers will have their default activation behaviour disabled, even if the player who attempts to activate one doesn't meet the `useBankerRank`.* 22 | ### Protection 23 | If these are enabled, the script will attempt to prevent its objects from being deleted. Note that having them disabled doesn't guarantee attempts to delete them will always be successful - other things might also block its deletion. 24 | - `denyBankerDelete` - If true, any object designated as a banker will be protected from permanent deletion. 25 | - `denyBankStorageDelete` - If true, any object which is being used as a player's storage will be protected from permanent deletion. 26 | ### Internal Stuff 27 | There are some options that are mostly used for internal things and shouldn't require any editing. 28 | - `baseObjectRefId` - The `refId` of the object that's used by this script's special storage containers. By default, a dead rat is used, since it's a non-despawning, infinite weight, undeletable container. This could be changed to be a regular sort of container (like a chest), if you wanted to enforce some sort of weight limit. 29 | - `baseObjectRecordType` - The object type for the object defined by `baseObjectRecordType`. You would only have to change this if you changed the `baseObjectRefId` to an object of a different type. 30 | - `storageCell` - The cell that the bank storage containers get placed in. The script uses an unreachable test cell by default, so no player should have access to it. The cell defined here is always loaded by the server. Do note that since this cell holds all of the player's bank storage containers *you should never delete this cell's data*. If you're using an automatic cell resetter, make sure that this cell is exempt from resets (if the script detects that Atkana's `CellReset` is being run on the server, it'll register the cell as exempt for that script automatically). 31 | - `recordRefId` - The id used for this script's special permanent record entry. There should be no reason why this would ever need changing, but if you do, ensure it doesn't contain any capitals. 32 | ### More Internal Stuff 33 | - `logging` - Whether the script should output normal information into the server log. 34 | - `debug` - Whether the script should output debug information into the server log. 35 | ### Language Support 36 | Almost every piece of text that's presented to the player can easily be changed by configuring the `lang` section. Keep the keys as they are, and edit their strings to read anything you want. Note that words beginning with `%` are special wildcards and shouldn't be translated (i.e. `%name` should stay as `%name`) - the script will automatically replace any instance of them with special text. 37 | 38 | ## Installation 39 | ### General 40 | - Save `kanaBank.lua` into `server/scripts/custom` 41 | ### Edits to `customScripts.lua` 42 | - kanaBank = require("custom.kanaBank") 43 | -------------------------------------------------------------------------------- /0.7/kanaHousing/readme.md: -------------------------------------------------------------------------------- 1 | # kanaHousing 2 | My own take on player housing, inspired by mupf's realEstate. 3 | ## Features 4 | * Players can buy and manage their own houses through a GUI. 5 | * Locking - Players can lock their houses, preventing others from entering. The owner, co-owners, and admins may enter the locked house whereas others will be turned away. Includes the ability to designate cells as important for travelling through (if entering it is required for a quest, for example), which allows regular members to enter while the cell is locked, so you don't have to worry so much about what places you designate as houses. 6 | * Owners can warp to any of their owned houses, though this feature can be disabled in the configs. 7 | * House owners can add co-owners, who can place or remove items in the house as well as pass through when the house is locked. 8 | * Admin GUI for creating, defining, and managing houses while in game. 9 | * While not strictly enforced, the script will report any players taking items from other player's houses in the server log, though it's not too difficult to expand what's already there if you want to do something with those dirty thieves. The script also differentiates between players taking regular items, and them taking items marked in the cell's data as quest items (the latter is okay). 10 | * Support for cell resets - Any doors associated with a house will automatically unlock if they should ever be locked, players can be allowed to pass through cells associated with quests, and the foundation has been laid for a server script to use to allow targeted resetting 11 | * Support for kanaFurniture. Owners and Co-owners are automatically given permission to place furniture in their houses. 12 | ## Usage 13 | ### Commands 14 | * `/houseinfo` - Use while in a house to view information on the house, as well as purchase it, if available. 15 | * `/house` - Used by players to view a list of all available houses on the server, as well as manage the settings of the houses that they own. 16 | * `/adminhouse` - Used by admins to edit and create new houses. 17 | 18 | ### Editing the files 19 | Some features, such as the reset info and a house's doors require manually editing the script's data file (found in data/kanaHousing.json). The structures of the script's data is outlined in the comments of the script's `createNewHouse` and `createNewCell`, if you wish to know how it all works. The following is an example of the script's data configured to include the single house: Chun-Ook, the boat in Ebonheart. It was chosen because it features two locked exterior doors, one "regular" cell (the cabin); one cell that requires passing through for a quest as well as an interior locked door (the upper deck); and one cell that requires passing through, contains quest items that'd need resetting (if you wish for multiple players to do the quest), and contains owned containers (the lower deck). 20 | ``` 21 | { 22 | "cells":{ 23 | "Chun-Ook, Upper Level":{ 24 | "house":"Ebonheart, Chun-Ook", 25 | "ownedContainers":false, 26 | "name":"Chun-Ook, Upper Level", 27 | "requiredAccess":true, 28 | "resetInfo":[], 29 | "requiresResets":false 30 | }, 31 | "Chun-Ook, Lower Level":{ 32 | "house":"Ebonheart, Chun-Ook", 33 | "ownedContainers":true, 34 | "name":"Chun-Ook, Lower Level", 35 | "requiredAccess":true, 36 | "resetInfo":[{ 37 | "instruction":"refill", 38 | "refId":"crate_01_limeware_uniqu", 39 | "refIndex":"297593-0" 40 | }], 41 | "requiresResets":true 42 | }, 43 | "Chun-Ook, Cabin":{ 44 | "house":"Ebonheart, Chun-Ook", 45 | "ownedContainers":false, 46 | "name":"Chun-Ook, Cabin", 47 | "requiredAccess":false, 48 | "resetInfo":[], 49 | "requiresResets":false 50 | } 51 | }, 52 | "owners":[], 53 | "houses":{ 54 | "Ebonheart, Chun-Ook":{ 55 | "name":"Ebonheart, Chun-Ook", 56 | "cells":{ 57 | "Chun-Ook, Upper Level":true, 58 | "Chun-Ook, Lower Level":true, 59 | "Chun-Ook, Cabin":true 60 | }, 61 | "price":5000, 62 | "outside":{ 63 | "pos":{ 64 | "y":-102871.0859375, 65 | "x":20961.751953125, 66 | "z":106.32440185547 67 | }, 68 | "cell":"2, -13" 69 | }, 70 | "doors":{ 71 | "2, -13":[{ 72 | "refIndex":"294940-0", 73 | "refId":"ex_de_ship_cabindoor" 74 | },{ 75 | "refIndex":"297456-0", 76 | "refId":"ex_de_ship_trapdoor" 77 | }], 78 | "Chun-Ook, Upper Level":[{ 79 | "refIndex":"297541-0", 80 | "refId":"in_de_shipdoor_toplevel" 81 | }] 82 | }, 83 | "inside":{ 84 | "pos":{ 85 | "y":-269.77154541016, 86 | "x":-113.86807250977, 87 | "z":-172.68469238281 88 | }, 89 | "cell":"Chun-Ook, Cabin" 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | 97 | ### Scripts 98 | There are a number of functions exposed for other scripts to use, which can be found towards the bottom of the script (more can be easily added) - I will probably try to write up what they are and what they do here eventually. 99 | 100 | ## Installation 101 | ### General 102 | - Save `kanaHousing.lua` into `server/scripts/custom` 103 | ### Edits to `customScripts.lua` 104 | - kanaHousing = require("custom.kanaHousing") 105 | 106 | If you have kanaFurniture installed, uncomment (remove the -- at the beginning) the line `kanaFurniture = require("custom.kanaFurniture")` that is after this installation info box in the `lua` file. Requires kanaFurniture release 3 or later. 107 | 108 | ## Known Issues 109 | I don't know of any issues and have tried to test everything to make sure it works, but it's possible that something slipped through the net and made it into the release. If you run into any problems, feel free to contact me so I can get things fixed! You're most likely to find me lurking in the [tes3mp Discord channel](https://discord.gg/ECJk293). 110 | -------------------------------------------------------------------------------- /0.7/kanaMOTD/kanaMOTD.json: -------------------------------------------------------------------------------- 1 | { 2 | "mainMessage":"This is the message that is used for the MOTD text", 3 | "title":"This is the title for a MOTD message box" 4 | } 5 | -------------------------------------------------------------------------------- /0.7/kanaMOTD/kanaMOTD.lua: -------------------------------------------------------------------------------- 1 | -- kanaMOTD - Release 2 - For tes3mp 0.7-prerelease 2 | -- Adds a MOTD message. 3 | 4 | --[[ INSTALLATION: 5 | 1) Save this file as "kanaMOTD.lua" in scripts/custom 6 | 2) Save the json file as "kanaMOTD.json" in data/custom 7 | 3) Add [ kanaMOTD = require("custom.kanaMOTD") ] to the top of customScripts.lua 8 | ]] 9 | 10 | local scriptConfig = {} 11 | 12 | scriptConfig.loadFromFile = false -- If true, the script will load and use the contents of kanaMOTD.json for the message and titles 13 | scriptConfig.showInChat = true -- If true, the message will be printed into the player's chat 14 | scriptConfig.showMessageBox = true -- If true, the player will be shown a message box upon joining that displays the MOTD 15 | 16 | -- The following are the string that'll be used if loadFromFile is set to false 17 | scriptConfig.mainMessage = "This is a [#yellow]MOTD[#default] message" 18 | scriptConfig.motdWindowTitle = "=== MOTD ===" 19 | 20 | --------------------------------------------------------------------------------------- 21 | jsonInterface = require("jsonInterface") 22 | require("color") 23 | --------------------------------------------------------------------------------------- 24 | local Methods = {} 25 | 26 | local MOTDmessage 27 | local MOTDtitle 28 | 29 | local lowerColors = {} 30 | 31 | -- Used to replace specialised color dealies with the actual color code 32 | Methods.ProcessText = function(text) 33 | local function replacer(wildcard) 34 | local lc = string.lower(wildcard) 35 | if lowerColors[lc] then 36 | -- Was a valid color code 37 | return lowerColors[lc] 38 | else 39 | -- Just happened to be a string that matched the color code signifier we're using 40 | return ("[##" .. wildcard .. "]") 41 | end 42 | end 43 | 44 | return text:gsub("%[#(%w+)%]", replacer) 45 | end 46 | 47 | Methods.Load = function() 48 | local loadedData = jsonInterface.load("custom/kanaMOTD.json") 49 | MOTDmessage = loadedData.mainMessage 50 | MOTDtitle = loadedData.title 51 | end 52 | 53 | Methods.ShowMOTD = function(eventStatus, pid) 54 | -- If configured to load from file, refresh the message in case the file has been changed 55 | if scriptConfig.loadFromFile then 56 | Methods.Load() 57 | end 58 | 59 | local processedMessage = Methods.ProcessText(MOTDmessage) 60 | local processedTitle = Methods.ProcessText(MOTDtitle) 61 | 62 | if scriptConfig.showInChat then 63 | tes3mp.SendMessage(pid, color.Warning .. "MOTD: " .. color.Default .. processedMessage .. color.Default .. "\n") 64 | end 65 | 66 | if scriptConfig.showMessageBox then 67 | local boxMessage = "" 68 | -- Only add the title in if it isn't blank 69 | if processedTitle ~= "" then 70 | boxMessage = boxMessage .. processedTitle .. color.Default .. "\n" 71 | end 72 | -- Add the main message 73 | boxMessage = boxMessage .. processedMessage 74 | 75 | tes3mp.CustomMessageBox(pid, -1, boxMessage, "Ok") 76 | end 77 | end 78 | 79 | Methods.Init = function() 80 | -- Load in the data for the messages 81 | -- That either means loading from the json, or porting in the messages from the config 82 | if scriptConfig.loadFromFile then 83 | Methods.Load() 84 | else 85 | MOTDmessage = scriptConfig.mainMessage 86 | MOTDtitle = scriptConfig.motdWindowTitle 87 | end 88 | 89 | -- Setup lowercase key colors 90 | for key, colorCode in pairs(color) do 91 | lowerColors[string.lower(key)] = colorCode 92 | end 93 | end 94 | 95 | --------------------------------------------------------------------------------------- 96 | 97 | customEventHooks.registerHandler("OnServerPostInit", Methods.Init) 98 | customEventHooks.registerHandler("OnPlayerAuthentified", Methods.ShowMOTD) 99 | 100 | --------------------------------------------------------------------------------------- 101 | return Methods 102 | -------------------------------------------------------------------------------- /0.7/kanaMOTD/readme.md: -------------------------------------------------------------------------------- 1 | # kanaMOTD 2 | Displays a MOTD message to every player who joins the server. 3 | 4 | *Currently written for a version of 0.7-prerelease* 5 | 6 | ## Configuration 7 | Configuration can be done from within the file itself. Here is a list of all the current config options: 8 | - **scriptConfig.loadFromFile** - If true, the script will load and use the contents of `kanaMOTD.json` for the message and titles. Use this if you plan on being able to change the server's MOTD without having to take it offline. 9 | - **scriptConfig.showInChat** - If true, the MOTD message will be printed into the player's chat. 10 | - **scriptConfig.showMessageBox ** - If true, the MOTD will appear in a message box. 11 | If `loadFromFile` is set to false, then the following are the strings that will be used for the MOTD: 12 | - **scriptConfig.mainMessage** - The MOTD message 13 | - **scriptConfig.motdWindowTitle** - The title that will appear before the MOTD in message boxes (only relevant if `showMessageBox` is `true`) 14 | 15 | ## Usage 16 | The script itself works automatically, displaying the MOTD to all players who join. Depending on your configuration, there are different variables that you have to edit for your MOTD: 17 | - If `loadFromFile` is enabled, you need to edit `mainMessage` and `title` inside `kanaMOTD.json` 18 | - Otherwise, you need to edit `scriptConfig.mainMessage` and `scriptConfig.motdWindowTitle` inside `kanaMOTD.lua` 19 | ### Formatting 20 | - The script includes its own method of coloring text. Use the color name as it appears in `color.lua` inside square brackets with a hash before it e.g. `[#red]`, to change the text's color. Alternatively you can just do the normal method of coloring and use the color codes themselves... 21 | - Newlines can be made with `\n`. 22 | - If you leave the title as a blank string (e.g. `""`), then the script won't try to use it in the message box. 23 | - Depending on the format you're editing, you may have to escape certain characters for what you've entered to work properly, though I'll leave that for you to discover yourselves ;P 24 | 25 | ## Installation 26 | ### General 27 | - Save `kanaMOTD.lua` into your `server/scripts/custom` folder 28 | - Save `kanaMOTD.json` into your `server/data/custom` folder 29 | ### Edits to `customScripts.lua` 30 | - `kanaMOTD = require("custom.kanaMOTD")` 31 | -------------------------------------------------------------------------------- /0.7/kanaRevive/readme.md: -------------------------------------------------------------------------------- 1 | # kanaRevive 2 | Players enter a downed state instead of dying. Other players can activate them to revive them before they bleedout. 3 | 4 | *Currently written for a version of 0.7-aplha* 5 | 6 | ## Usage 7 | When a player is killed, instead of dying, they enter a "downed" state - with a countdown until they die properly commencing (time, and whether there is a countdown are configurable). If another player activates them, the player is instantly revived, with their health, magicka, and fatigue being set to a value based on configuration options. While downed, the player has access to the `/die` command, which they can use to instantly trigger their death. If a player logs out during their bleedout countdown, they will resume from the point they left off when they next log in. 8 | 9 | ## Configuration 10 | Configuration is done from within the file itself, by altering the `scriptConfig` values. 11 | ### Bleedout Options 12 | There are two options for governing how bleedout works: 13 | - `useBleedout` - If set to `true`, then a countdown will begin once a player is downed. If the timer runs out before the player is revived, they die properly. If set to `false`, the countdown doesn't begin, and the players will remain downed indefinitely until they're either revived, or they use the `/die` command. 14 | - `bleedoutTime` - This determines the time (in seconds) that a player has before their countdown runs out. Obviously only relevant if `useBleedout` is set to `true`. 15 | - `allowReviveWithPermadeath` - An option for permadeath servers to allow players to enter a downed state before dying. Set to `true` to enable this. 16 | ### Broadcast Options 17 | Announcements are made into chat whenever: a player enters the downed state, a player is revived, a player who was downed dies. The "range" that these messages can be heard by other players can be set to three values: 18 | - `"server"` - The message is broadcast to everyone on the server. 19 | - `"cell"` - The message is broadcast to everyone who has the cell it happened in loaded. 20 | - `"none"` - The message isn't broadcast. 21 | 22 | Each announcement type can be edited independently. The configuration options for these announcement types are: 23 | - `playerDownedAnnounceRadius` - Governs messages about a player entering the downed state. 24 | - `reviveAnnounceRadius` - Governs messages about a player being revived. 25 | - `playerDiedAnnounceRadius` - Governs messages about a player who was downed dying (bleeding out). 26 | 27 | *(Note that players involved in the events receive their own special messages, rather than the general broadcast).* 28 | ### Stat Options 29 | The value that each of a player's main stats (health, magicka, and fatigue) are set to after being revived can be handled in multiple ways. The mode that each stat uses can be edited by changing the configuration options `revivedHealthMode`, `revivedMagickaMode`, and `revivedFatigueMode` for health, magicka, and fatigue respectively. The valid modes are: 30 | - `"set"` - The stat is set to a particular value. What value this will be is governed by the configuration options `setModeHealth`, `setModeMagicka`, and `setModeFatigue`. 31 | - `"preserve"` - The stat will remain as it was. 32 | - `"percent"` - The stat is set to be a percent of the player's maximum value for that stat. What percent modifier is used is governed by the configuration options `percentModeHealth`, `percentModeMagicka`, and `percentModeFatigue`. Note that the values used are multipliers - `0.1` represents 10%, `1` represents 100% 33 | 34 | *(Note that the script will automatically ensure that the stat values are clamped within the stat's normal bounds, and that the value used for health will, at minimum, be 1).* 35 | ### Revive Marker Options 36 | To overcome some current problems, a special activatable object can also be made spawned for any player who wasn't in the cell when the player was downed. 37 | - `useMarkers` - If set to `true`, then a special marker object will also be spawned when a player is downed. Anyone who activates either the object or the downed player will revive them. 38 | - `markerModel` - The model to use as the revive marker. By default, it's a spoopy skellington. 39 | - `baseObjectType` - The record type of the revive marker. Honestly won't ever need changing from its default value of `"miscellaneous"` 40 | - `recordRefId` - The ID used for the revive marker's permanent record. Won't need changing. 41 | 42 | ### Language Support 43 | Almost every piece of text that's presented to the player can easily be changed by configuring the `lang` section. Keep the keys as they are, and edit their strings to read anything you want. Note that words beginning with `%` are special wildcards (? I think that's the term) and shouldn't be translated (i.e. `%name` should stay as `%name`) - the script will automatically replace any instance of them with special text. 44 | 45 | For example: `revivedReceiveMessage` determines the message that a player receives when they are revived by somebody. In this case, `%name` is used as a placeholder for the name of the player. So if the player who revived you was called `N'wah`, then the message `You have been revived by %name.` would become `You have been revived by N'wah.` 46 | 47 | ## Installation 48 | ### General 49 | - Save `kanaRevive.lua` into `server/scripts` 50 | ### Edits to `customScripts.lua` 51 | - Add this line: `kanaRevive = require("custom.kanaRevive")` 52 | 53 | ## Known Issues 54 | A script like this is awkward to test on my own, so there is the possibility that bugs might've slipped through my testing. Contact me if you find anything! 55 | - Not an issue per se, but worthy of mention: Reviving a player is instant. I could've added some time delay faff, but I preferred not to :P. It's possible that I *could* change that in the future if there was enough demand for it. 56 | - Sometimes, if another player is downed in a different cell, it's possible that when you enter the cell, you won't be able to see the player's body. If `useMarkers` is enabled, then a special activatable object will be spawned alongside their body - you can activate either the special object or the player to revive them. 57 | - It's possible that through niche circumstances, a revive marker might remain even when not linked to a player. Simply using the marker will clear it away harmlessly. 58 | -------------------------------------------------------------------------------- /0.7/kanaStartingItems/kanaStartingItems.lua: -------------------------------------------------------------------------------- 1 | -- kanaStartingItems - Release 2 - For tes3mp 0.7-alpha 2 | -- Grant newly created characters some configurable starting items based on their race, class, skills, favored armor, and birthsign 3 | 4 | --[[ INSTALLATION: 5 | 1) Save this file as "kanaStartingItems.lua" in server/scripts/custom 6 | 2) Add [ kanaStartingItems = require("custom.kanaStartingItems") ] to customScripts.lua 7 | ]] 8 | 9 | --[[ NOTES: 10 | Things this doesn't care about/support: 11 | - Major skills 12 | - Minor skills 13 | - Beast race difference 14 | - Random tables 15 | - Any nuances of an item beyond the item and how many (so no partially-damaged items, or soulgems enchanted with a specific soul) 16 | ]] 17 | 18 | local scriptConfig = {} 19 | -- The skill level thresholds used to determine when to award items based on skill 20 | -- By default they align to the minimum skill level that picking major/minor skills will give a character 21 | scriptConfig.highSkillThreshold = 30 22 | scriptConfig.lowSkillThreshold = 15 23 | 24 | -- The following two options govern whether the player should be notified that they've been given extra items. 25 | scriptConfig.informPlayers = true -- If true, players will be told that they've been given extra starting items 26 | scriptConfig.message = "You have been given some extra starting items based on your character." -- The message that they're given if informPlayers is true 27 | 28 | -- The last remaining options are all configuration options that you can use to dictate what items players receive based on each category of criteria 29 | -- Some examples are included for each, though you can edit/remove what you don't want 30 | -- You'll need some knowledge about how lua tables work to make the edits, though you might be able to intuit what to do from the examples. 31 | 32 | -- Items given to a player based on their race 33 | scriptConfig.raceItems = { 34 | ["khajiit"] = { 35 | {refId = "ingred_moon_sugar_01", count = 3}, 36 | {refId = "apparatus_a_spipe_01", count = 1}, 37 | }, 38 | } 39 | 40 | -- Items given to a player based on their class 41 | -- Note that only non-custom classes are supported with this 42 | scriptConfig.classItems = { 43 | ["thief"] = { 44 | {refId = "pick_apprentice_01", count = 1}, 45 | {refId = "probe_apprentice_01", count = 1}, 46 | }, 47 | } 48 | 49 | -- Items given to a player based on their high skills (as determined by scriptConfig.highSkillThreshold) 50 | -- Note that if a character's skill is deemed as a "high skill", it ISN'T also counted as a low skill 51 | scriptConfig.highSkillItems = { 52 | ["acrobatics"] = { 53 | {refId = "hoptoad ring", count = 1}, 54 | }, 55 | } 56 | 57 | -- Items given to a player based on their low skills (as determined by scriptConfig.lowSkillThreshold) 58 | -- Note that if a character's skill is deemed as a "low skill", it ISN'T also counted as a high skill 59 | scriptConfig.lowSkillItems = { 60 | ["marksman"] = { 61 | {refId = "chitin throwing star", count = 20}, 62 | }, 63 | } 64 | 65 | -- Items given to a player based on their highest armor skill. 66 | -- Note that if two skills are equal, the highest armor skill is picked arbitrarily 67 | -- Armor skills are: heavyarmor, mediumarmor, lightarmor, unarmored 68 | scriptConfig.armorItems = { 69 | ["lightarmor"] = { 70 | {refId = "fur_colovian_helm", count = 1}, 71 | }, 72 | } 73 | 74 | -- Items given to players based on their birthsign 75 | scriptConfig.birthsignItems = { 76 | ["wombburned"] = { 77 | {refId = "p_restore_magicka_c", count = 3}, 78 | }, 79 | } 80 | 81 | -- General items are given to every player 82 | scriptConfig.generalItems = { 83 | {refId = "gold_001", count = 200}, 84 | } 85 | 86 | --------------------------------------------------------------------------------------- 87 | local Methods = {} 88 | 89 | local RaceItems = {} 90 | local ClassItems = {} 91 | local HighSkillItems = {} 92 | local LowSkillItems = {} 93 | local ArmorItems = {} 94 | local BirthsignItems = {} 95 | local GeneralItems = {} 96 | 97 | inventoryHelper = require("inventoryHelper") 98 | ------------- 99 | Methods.RegisterRacialItem = function(raceId, itemRefId, count) 100 | local raceId = string.lower(raceId) 101 | 102 | -- Create main table if it doesn't exist 103 | if RaceItems[raceId] == nil then RaceItems[raceId] = {} end 104 | 105 | table.insert(RaceItems[raceId], {refId = itemRefId, count = (count or 1)}) 106 | end 107 | 108 | Methods.RegisterClassItem = function(className, itemRefId, count) 109 | local className = string.lower(className) 110 | 111 | -- Create main table if it doesn't exist 112 | if ClassItems[className] == nil then ClassItems[className] = {} end 113 | 114 | table.insert(ClassItems[className], {refId = itemRefId, count = (count or 1)}) 115 | end 116 | 117 | 118 | Methods.RegisterHighSkillItem = function(skillName, itemRefId, count) 119 | local skillName = string.lower(skillName) 120 | 121 | -- Create main table if it doesn't exist 122 | if HighSkillItems[skillName] == nil then HighSkillItems[skillName] = {} end 123 | 124 | table.insert(HighSkillItems[skillName], {refId = itemRefId, count = (count or 1)}) 125 | end 126 | 127 | Methods.RegisterLowSkillItem = function(skillName, itemRefId, count) 128 | local skillName = string.lower(skillName) 129 | 130 | -- Create main table if it doesn't exist 131 | if LowSkillItems[skillName] == nil then LowSkillItems[skillName] = {} end 132 | 133 | table.insert(LowSkillItems[skillName], {refId = itemRefId, count = (count or 1)}) 134 | end 135 | 136 | Methods.RegisterArmorItem = function(armorType, itemRefId, count) 137 | local armorType = string.lower(armorType) 138 | 139 | -- Create main table if it doesn't exist 140 | if ArmorItems[armorType] == nil then ArmorItems[armorType] = {} end 141 | 142 | table.insert(ArmorItems[armorType], {refId = itemRefId, count = (count or 1)}) 143 | end 144 | 145 | Methods.RegisterBirthsignItem = function(birthsignId, itemRefId, count) 146 | local birthsignId = string.lower(birthsignId) 147 | 148 | -- Create main table if it doesn't exist 149 | if BirthsignItems[birthsignId] == nil then BirthsignItems[birthsignId] = {} end 150 | 151 | table.insert(BirthsignItems[birthsignId], {refId = itemRefId, count = (count or 1)}) 152 | end 153 | 154 | Methods.RegisterGeneralItem = function(itemRefId, count) 155 | table.insert(GeneralItems, {refId = itemRefId, count = (count or 1)}) 156 | end 157 | 158 | ------------- 159 | local function addItem(pid, refId, count) 160 | local inventory = Players[pid].data.inventory 161 | 162 | inventoryHelper.addItem(inventory, refId, (count or 1), -1, -1, "") 163 | end 164 | 165 | Methods.OnCharacterCreated = function(eventStatus, pid) 166 | local gaveItems = false --Used to detemine if the script has given the player any items 167 | 168 | -- Add racial items 169 | local playerRace = string.lower(Players[pid].data.character.race) 170 | 171 | if RaceItems[playerRace] ~= nil then 172 | for index, itemInfo in ipairs(RaceItems[playerRace]) do 173 | addItem(pid, itemInfo.refId, itemInfo.count) 174 | end 175 | 176 | gaveItems = true 177 | end 178 | 179 | -- Add class items 180 | local playerClass = string.lower(Players[pid].data.character.class) 181 | 182 | if playerClass ~= "custom" and ClassItems[playerClass] ~= nil then 183 | for index, itemInfo in ipairs(ClassItems[playerClass]) do 184 | addItem(pid, itemInfo.refId, itemInfo.count) 185 | end 186 | 187 | gaveItems = true 188 | end 189 | 190 | -- Do skill stuff 191 | local highSkills = {} 192 | local lowSkills = {} 193 | 194 | local bestArmorSkill 195 | local bestArmorSkillLevel = -1 196 | 197 | local armorSkills = {mediumarmor = true, heavyarmor = true, lightarmor = true, unarmored = true} 198 | 199 | for skillId, skillInfo in pairs(Players[pid].data.skills) do 200 | local skillId = string.lower(skillId) 201 | local level = skillInfo.base 202 | 203 | if level >= scriptConfig.highSkillThreshold then 204 | -- Skill is a high skill, add it to the list 205 | table.insert(highSkills, skillId) 206 | elseif level >= scriptConfig.lowSkillThreshold then 207 | -- Skill is a low skill, add it to the list 208 | table.insert(lowSkills, skillId) 209 | end 210 | 211 | -- Do checks to determine the best armor skill 212 | -- Check if is armor skill... 213 | if armorSkills[skillId] then 214 | -- If it is, then determine if it's higher than the previously recorded one 215 | -- No tiebreakers 216 | if level > bestArmorSkillLevel then 217 | -- It's higher, so record it as the best so far 218 | bestArmorSkill = skillId 219 | bestArmorSkillLevel = level 220 | end 221 | end 222 | end 223 | 224 | -- Add high skill items 225 | for index, skillId in ipairs(highSkills) do 226 | if HighSkillItems[skillId] ~= nil then 227 | for index, itemInfo in ipairs(HighSkillItems[skillId]) do 228 | addItem(pid, itemInfo.refId, itemInfo.count) 229 | end 230 | 231 | gaveItems = true 232 | end 233 | end 234 | 235 | -- Add low skill items 236 | for index, skillId in ipairs(lowSkills) do 237 | if LowSkillItems[skillId] ~= nil then 238 | for index, itemInfo in ipairs(LowSkillItems[skillId]) do 239 | addItem(pid, itemInfo.refId, itemInfo.count) 240 | end 241 | 242 | gaveItems = true 243 | end 244 | end 245 | 246 | -- Add armor items 247 | -- The player's best armor skill was determined during the skill loop earlier 248 | if ArmorItems[bestArmorSkill] ~= nil then 249 | for index, itemInfo in ipairs(ArmorItems[bestArmorSkill]) do 250 | addItem(pid, itemInfo.refId, itemInfo.count) 251 | end 252 | 253 | gaveItems = true 254 | end 255 | 256 | -- Add birthsign items 257 | local playerBirthsign = string.lower(Players[pid].data.character.birthsign) 258 | 259 | if BirthsignItems[playerBirthsign] ~= nil then 260 | for index, itemInfo in ipairs(BirthsignItems[playerBirthsign]) do 261 | addItem(pid, itemInfo.refId, itemInfo.count) 262 | end 263 | 264 | gaveItems = true 265 | end 266 | 267 | -- Add general items 268 | for index, itemInfo in ipairs(GeneralItems) do 269 | addItem(pid, itemInfo.refId, itemInfo.count) 270 | 271 | gaveItems = true 272 | end 273 | 274 | -- Update the player's inventory 275 | -- Note that we should really be using the item adding function, but there's no harm in being messy and doing it this way :P 276 | Players[pid]:LoadInventory() 277 | Players[pid]:LoadEquipment() -- Used to be required, otherwise the player spawns naked. Not sure if it's needed but I'm including it here just in case 278 | 279 | Players[pid]:Save() 280 | 281 | -- Send the player a message that they've been given some extra starting items, if configured to do so 282 | if scriptConfig.informPlayers and gaveItems then 283 | tes3mp.SendMessage(pid, scriptConfig.message .. "\n") 284 | end 285 | end 286 | 287 | Methods.Init = function() 288 | -- Load all of the information that the user entered in the scriptconfig 289 | 290 | -- Race Items 291 | for raceId, itemList in pairs(scriptConfig.raceItems) do 292 | for index, itemInfo in ipairs(itemList) do 293 | Methods.RegisterRacialItem(raceId, itemInfo.refId, (itemInfo.count or 1)) 294 | end 295 | end 296 | 297 | -- Class Items 298 | for classId, itemList in pairs(scriptConfig.classItems) do 299 | for index, itemInfo in ipairs(itemList) do 300 | Methods.RegisterClassItem(classId, itemInfo.refId, (itemInfo.count or 1)) 301 | end 302 | end 303 | 304 | -- High Skill Items 305 | for skillId, itemList in pairs(scriptConfig.highSkillItems) do 306 | for index, itemInfo in ipairs(itemList) do 307 | Methods.RegisterHighSkillItem(skillId, itemInfo.refId, (itemInfo.count or 1)) 308 | end 309 | end 310 | 311 | -- Low Skill Items 312 | for skillId, itemList in pairs(scriptConfig.lowSkillItems) do 313 | for index, itemInfo in ipairs(itemList) do 314 | Methods.RegisterLowSkillItem(skillId, itemInfo.refId, (itemInfo.count or 1)) 315 | end 316 | end 317 | 318 | -- Birthsign Items 319 | for birthsignId, itemList in pairs(scriptConfig.birthsignItems) do 320 | for index, itemInfo in ipairs(itemList) do 321 | Methods.RegisterBirthsignItem(birthsignId, itemInfo.refId, (itemInfo.count or 1)) 322 | end 323 | end 324 | 325 | -- Armor Items 326 | for armorTypeId, itemList in pairs(scriptConfig.armorItems) do 327 | for index, itemInfo in ipairs(itemList) do 328 | Methods.RegisterArmorItem(armorTypeId, itemInfo.refId, (itemInfo.count or 1)) 329 | end 330 | end 331 | 332 | -- General Items 333 | for index, itemInfo in ipairs(scriptConfig.generalItems) do 334 | Methods.RegisterGeneralItem(itemInfo.refId, (itemInfo.count or 1)) 335 | end 336 | 337 | end 338 | ------------- 339 | customEventHooks.registerHandler("OnServerPostInit", Methods.Init) 340 | customEventHooks.registerHandler("OnPlayerEndCharGen", Methods.OnCharacterCreated) 341 | ------------- 342 | return Methods 343 | -------------------------------------------------------------------------------- /0.7/kanaStartingItems/readme.md: -------------------------------------------------------------------------------- 1 | # kanaStartingItems 2 | Grant newly created characters some configurable starting items based on their race, class, skills, favored armor, and birthsign. 3 | 4 | *Currently written for a version of 0.7-alpha* 5 | 6 | ## Usage 7 | The script works automatically whenever a new character is created. By configuring the script you can govern extra items that a new player is given based off of their: 8 | - Race 9 | - Class - Note that only non-custom classes are supported by this script. 10 | - High Skills - A stand-in for major skills, since scripts can't know the major skills for non-custom classes without being dependant on more scripts. What's considered a "high skill" is set by the `highSkillThreshold` configuration option (*see: Configuration*). Note that if a skill is high enough to be considered a "high skill", it doesn't *also* count as being a "low skill" for the purposes of being given "low skill" items. 11 | - Low Skills - A stand-in for minor skills. As with "high skills" it has its own configuration option (`lowSkillThreshold`). 12 | - Birthsign 13 | - Best armor skill - The script determines which of the player's skills out of Heavy armor, Medium armor, Light armor, and unarmored is their best armor skill. If there are multiple skills tied at the same value, then the "best armor skill" is chosen arbitrarily. 14 | 15 | Additionally, there is another configuration option for adding items to all players, regardless of criteria (so for example, you could give players 200 gold to start off with). 16 | 17 | ## Configuration 18 | Configuration is done from within the file itself, by altering the `scriptConfig` values. 19 | ### General Options 20 | - **highSkillThreshold** - Any skills with a level higher than this are considered "high skills". By default this value is set to `30`, which is the base amount that is added by a major skill. 21 | - **lowSkillThreshold** - Any skills with a level higher than this (but lower than the `highSkillThreshold`) are considered "low skills". By default this value is set to `15`, which is the base amount that is added by a minor skill. 22 | - **informPlayers** - If `true`, any player who generates a character and receives extra items because of this script will be given a notification message in chat. This is useful to let players know that they've got new items, as well as eliminate some confusion if they notice that they have some abnormal starting items. 23 | - **message** - This is the message that players will receive in chat provided that `informPlayers` is set to `true` 24 | ### Item Configuration 25 | The configuration options `raceItems` (what races get), `classItems` (what classes get), `highSkillItems` (what characters with a high skill get), `lowSkillItems` (what classes with a low skill get), `armorItems` (what each armor specialist gets), `birthsignItems` (what characters of a birthsign get), and `generalItems` (what *all* characters get) all govern the items that players receive. You'll need some knowledge of how lua tables work to make the edits, though you might be able to intuit what to do from the examples given. You don't have to keep the example entries (in fact, you'll probably want to remove them) - they're mostly there just to serve as examples, as well as being what I used when testing, rather than being properly thought out suggestions. 26 | 27 | Items are listed inside a table, with each entry being a table containing the item's `refId` and `count` (how many) under their respective keys. With the exception of `generalItems`, each of the options requires entries to be within tables under certain keys. The keys you use for each are: 28 | - **raceItems** - The race's ID. E.g. `Dark Elf` for dark elves, `Orc` for orcs. 29 | - **classItems** - The class ID. E.g. `thief` for a thief 30 | - **highSkillItems** and **lowSkillItems** and **armorItems** - The ID of the skill as it would appear in a player's `json`. E.g. `Handtohand` for hand-to-hand, `Mediumarmor` for medium armor 31 | - **birthsignItems** - The birthsign's ID. E.g. `Wombburned` for The Atronach, `Mooncalf` for The Lover 32 | 33 | *(You may notice that the examples don't follow the exact capitalization for each ID type - this is fine, since the script doesn't care about capitalization)* 34 | 35 | ## Installation 36 | ### Save the Script 37 | Save the file as `kanaStartingItems.lua` inside your `server/scripts/custom` folder. 38 | ### Edits to `customScripts.lua` 39 | - Add `kanaStartingItems = require("custom.kanaStartingItems")` 40 | 41 | ## Script Methods 42 | There are Methods available for other scripts to add items to each of the categories. Check out the file itself to see what's available. 43 | -------------------------------------------------------------------------------- /0.7/readme.md: -------------------------------------------------------------------------------- 1 | # 0.7 tes3mp-scripts 2 | Collection of all my scripts for tes3mp 0.7 (or, at least, 0.7-prerelease). The information for some scripts can be found here, however larger ones may have their own separate readme in their own folder. Unless otherwise stated, you can find installation instructions at the top of each file. 3 | 4 | ## Scripts 5 | ### decorateHelp 6 | Todo 7 | ### flatModifiers 8 | Change the way that the attribute advancement modifiers on level up are calculated. It can be configured to give a flat bonus to all players, or a tailored bonus based on their class. Requires *classInfo*. 9 | #### Configuration 10 | There are a number of configuration options available to edit in the file itself. 11 | ##### General 12 | - **config.mode** ("basic" / "class") - Determines what mode the script should run in. If set to *basic*, the players will be given a set value to their attribute advancements (as dictated by **config.basicAttributeIncreases**). If set to *class*, the players will be given tailored attribute advancements based on aspects of their class (see *class mode* for the options) 13 | - **config.includeLuck** (true / false) - Determines whether or not to include Luck in the calculations. By default, Morrowind doesn't allow bonuses to Luck. 14 | ##### Basic Mode 15 | - **config.basicAttributeIncreases** - The number of increases towards stats that the script should fake. 16 | ##### Class Mode 17 | - **config.classBase** - How many advancements to use as a base 18 | - **config.classMajorSkillBonus** - How many advancements get added to an attribute per major skill governed by it 19 | - **config.classMinorSkillBonus** - How many advancements get added to an attribute per minor skill governed by it 20 | - **config.classAttributeBonus** - How many advancements get added to an attribute which is one of the class' major attributes 21 | #### Useful Information 22 | It takes the following number of advancements to achieve these levelup multipliers: 23 | 1-4 gives 2x, 5-7 gives 3x, 8-9 gives 4x, 10+ gives 5x. 24 | 25 | ## Resources 26 | ### classInfo 27 | A compilation of data on the game's base classes and some related functions for use in server scripts. NPC classes not yet implemented. 28 | #### Usage 29 | Needs to be *require*d somewhere. 30 | 31 | See the file itself for full information on/list of Methods. 32 | - **classInfo.GetPlayerClassData(pid)** - returns a table of information on the provided player's class (custom or default). 33 | - **classInfo.GetCustomClassData(pid)** -returns a table of information on the provided player's custom class. Should probably just use **classInfo.GetPlayerClassData(pid)** instead. 34 | - **classInfo.GetClassData(className)** - returns a table of information on the provided class. 35 | - **classInfo.GetGovernedAttribute(skillId)** - returns the attribute id of the attribute that governs the skill. 36 | - **classInfo.GetSpecializationName(specializationId)** - returns the name of the specialization that has the provided specialization id (e.g. 0 returns "Combat") 37 | 38 | #### Useful Default Functions 39 | - **tes3mp.GetAttributeName(attributeId)** 40 | - **tes3mp.GetSkillName(skillId)** 41 | - **tes3mp.GetAttributeId(attributeName)** 42 | - **tes3mp.GetSkillId(skillName)** 43 | -------------------------------------------------------------------------------- /0.8/readme.md: -------------------------------------------------------------------------------- 1 | Ports of some 0.7 scripts (currently entirely done by others). Check the 0.7 folder for the proper documentation on the scripts. 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /Porting Scripts to 0.7 alpha and How Does.md: -------------------------------------------------------------------------------- 1 | # Porting Scripts to 0.7 (alpha) and How Does 2 | There have been some minor changes script-compatibility-wise with the new 0.7 release, with a lot of the common required edits to 0.6.2 scripts being very straightforward. I've provided a list of the changes that I'm aware of for those interested in updating their own scripts, or looking to see if they can get old 0.6.2 scripts working on their 0.7 servers. 3 | ### server.lua has become serverCore.lua 4 | From the perspective of installing scripts, not much has changed beyond the name of the file (well, with the exception of one thing, which I'll get to next). If the script's installation instructions say to do something to `server.lua`, do it to `serverCore.lua` instead, with the exception of... 5 | ### Chat commands have been moved to commandHandler.lua 6 | If an installation instruction had previously said something nebulous about adding a lump of code to the "elseif chain for commands in `OnPlayerSendMessage` inside `server.lua`" (who even writes these?), it is now instead added to the "elseif chain for commands in `commandHandler.ProcessCommand` inside the file `commandHandler.lua`". 7 | ### actionTypes.lua has become enumerations.lua 8 | I'm not aware of many scipts that previously used actionTypes, but porting them to the new version is simple. There a couple of easy ways to do so, with one of them being to run a find and replace, changing any instance of `actionTypes` into `enumerations`. 9 | ### The player setting "admin" is now "staffRank" 10 | The name used internally (and in player save files) that marks what level of admin privilege a player character has has changed from `admin` to `staffRank`. In the niche cases that a scripter has written their script to make a direct reference to that variable, instead of using the special functions for checking a player's ranks (oops, my bad :P), you'll have to make edits in the code. You should be safe by just replacing any instance of `data.settings.admin` with `data.settings.staffRank`. 11 | 12 | For any interested scripters reading, the aforementioned special functions in 0.7 are part of the player class - `IsModerator()`, `IsAdmin()`, `IsServerOwner()`, and `IsServerStaff()`. Also: when the game loads a player save file from a previous version, it'll automatically update it to using the term `staffRank`, in case you were wondering :P 13 | ### InputDialog now requires an extra argument 14 | The way `InputDialog`s work (the thing that pops up and asks you to enter some text) have changed, and any code that used it previously needs updating. Thankfully, the edit is very straightforward: 15 | * Search through the code for any instance of `tes3mp.InputDialog` (if there aren't any, then you don't need to bother doing this) 16 | * For each instance, add `,""` directly before the closing bracket (the `)`). For example, if the line was originally `tes3mp.InputDialog(pid, guiId, message)`, it'll become `tes3mp.InputDialog(pid, guiId, message,"")` 17 | 18 | For scripters: The new - fourth - argument is a string that'll be displayed beneath the text input area - like how the warning about server owners being able to read your passwords appears when you make a new account (I believe). If you don't want anything displayed, passing an empty string should suffice. 19 | ### os.getenv("MOD_DIR") becomes tes3mp.GetModDir() 20 | Regarding [this commit](https://github.com/TES3MP/CoreScripts/commit/c43f42b7d35f026e1f9b5e91a742d84f1b0d23cd) you have to change `os.getenv("MOD_DIR")` to `tes3mp.GetModDir()`. This is most common in places where `jsonInterface` is loading files and needs a path to your `/data/` folder. 21 | ### myMod has split into logicHandler and eventHandler 22 | Previously, `myMod` was responsible for some logic-based stuff, as well as processing events. Now, the logic-based stuff is in `logicHandler.lua` and the event processing stuff is in `eventHandler.lua`. There probably aren't many scripts that were originally using myMod's event-processing functions themselves, beyond perhaps requiring alterations to them as part of installing the script. If a script *does* require edits to any of those parts, you can try making those edits to `eventHandler.lua`. Be aware that the instructions that the script gives are most likely to be explicitly for the version the script was written for, and doing those same edits for this version might break things. 23 | 24 | If the script itself was *only* using the functions that are part of `logicHandler`, there's a cheesy edit you can make to the script that might be all that's needed to get it running: 25 | * Search the script for a mention of `require("myMod")`, if there is one, delete it (be sure to delete the whole line, in the case it's something like `myMod = require("myMod")` 26 | * In it's place (if it was there) or near the top of the file (if it wasn't), add the line `local myMod = require("logicHandler")` 27 | 28 | With these changes, the script *might* work, provided that a) the script was only using `myMod` for the things that ended up in `logicHandler`, and b) the way the `logicHandler` functions that the script uses haven't changed in... uh, function, significantly between versions. If either of these things aren't true then things might break. 29 | If you're a scripter looking for actual non-hacky advice on porting your script: 30 | * Get rid any `require`s for `myMod` 31 | * Require either or both of `logicHandler` and `eventHandler` as you would have `myMod` 32 | * Go through and update all your calls to `myMod` to use the correct script 33 | * Test/Read through the functions to see if anything's changed between versions, and edit your script appropriately 34 | 35 | Note that it's entirely possible that following this advice may break things, and it was all written by somebody who hasn't actually used 0.7, nor tried the changes they're suggesting. If you've got edits to make/suggestions of things to add, send me a message, or just make a pull request adding the changes - this is github, after all ;P 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tes3mp-scripts 2 | Collection of all my scripts for tes3mp. **Important note:** Many of these scripts are for v0.6, and only exist here so that links don't break. For scripts created for versions beyond v0.6, look in the appropriately named folders. 3 | ## Scripts 4 | ### flatModifiers 5 | [Steam Disucssion](http://steamcommunity.com/groups/mwmulti/discussions/0/3182216552768549150/) - Change the way that the attribute advancement modifiers on level up are calculated. 6 | 7 | ### markRecall 8 | [Steam Discussion](https://steamcommunity.com/groups/mwmulti/discussions/0/1488861734096173445/) - A patch that adds the functionality of marking and recalling via server command, useful until the spells get fixed. 9 | 10 | ### salesChest 11 | [Steam Discussion](https://steamcommunity.com/groups/mwmulti/discussions/0/1483232961046461458/) - Players can claim containers and sell its contents. 12 | 13 | ### serverWarp 14 | [Steam Discussion](https://steamcommunity.com/groups/mwmulti/discussions/0/1488861734099531437/) - Adds the ability to save locations and later warp to them. 15 | 16 | ## Resources 17 | ### itemInfo 18 | [Steam Discussion](https://steamcommunity.com/groups/mwmulti/discussions/0/1483232961046419094/) - A compilation of data on the game's items and some related functions for use in server scripts. Not all items are currently implemented. 19 | 20 | ### classInfo 21 | [Steam Discussion](https://steamcommunity.com/groups/mwmulti/discussions/0/1483233503861870523/) - A compilation of data on the game's base classes and some related functions for use in server scripts. NPC classes not yet implemented. 22 | #### Usage 23 | Needs to be *require*d somewhere. 24 | 25 | See the file itself for full information on/list of Methods. 26 | 27 | **classInfo.GetPlayerClassData(pid)** - returns a table of information on the provided player's class (custom or default). 28 | 29 | **classInfo.GetCustomClassData(pid)** -returns a table of information on the provided player's custom class. Should probably just use **classInfo.GetPlayerClassData(pid)** instead. 30 | 31 | **classInfo.GetClassData(className)** - returns a table of information on the provided class. 32 | 33 | **classInfo.GetGovernedAttribute(skillId)** - returns the attribute id of the attribute that governs the skill. 34 | 35 | **classInfo.GetSpecializationName(specializationId)** - returns the name of the specialization that has the provided specialization id (e.g. 0 returns "Combat") 36 | 37 | #### Useful Default Functions 38 | **tes3mp.GetAttributeName(attributeId)** 39 | 40 | **tes3mp.GetSkillName(skillId)** 41 | 42 | **tes3mp.GetAttributeId(attributeName)** 43 | 44 | **tes3mp.GetSkillId(skillName)** 45 | 46 | ## Legacy Scripts 47 | ### salesChestGlobalHack 48 | Just a quick hack of sales chest to add the global mode, before I actually added it in a less-hacky manner to the main script. Don't use this one. I don't even know why it's still here :P 49 | 50 | ### flatModifiersBasic 51 | [Steam Discussion](https://steamcommunity.com/groups/mwmulti/discussions/0/1483233503861929448/) - Original version of flatModifiers, which wasn't dependant on classInfo. 52 | -------------------------------------------------------------------------------- /Random Assortment of tes3mp Scripting Knowledge.md: -------------------------------------------------------------------------------- 1 | Here's a random assortment of my acquired knowledge in regards to tes3mp scripting. It might not be very coherent or useful, but maybe somebody might learn from it. 2 | 3 | ## Version 0.6.1 4 | ### Gotcha - Getting The Character's Name 5 | When logging in as a character, the login dialog doesn't care about capitalization (for example, my character's login is "N'wah", but I could enter "n'WaH" and the game will still accept it). At this point, the game sets *the name that was entered* as the player's name for the purpose of getting the name via `tes3mp.GetName(pid)` or `Players[pid].name` and *not* the character's login name (retrieved via `Players[pid].data.login.name`). If you're storing any data using the player's name, you should make sure to use the player's *login name*, since the *player's name* (capitalization-wise) can vary by login. 6 | 7 | Alternatively you can circumvent most of this by using myMod's `myMod.GetPlayerByName(targetName)` :P 8 | 9 | ### Gotcha - Detecting An Object 10 | If you're using Cell:ContainsObject(refIndex) to detect if an object exists in that cell, and that object is a data file object, then you also need to check that the object doesn't have a delete packet associated with it. 11 | So for example: 12 | ``` 13 | if LoadedCells[cell]:ContainsObject(refIndex) and not tableHelper.containsValue(LoadedCells[cell].data.packets.delete, refIndex) then 14 | --Whatever 15 | end 16 | ``` 17 | ### Gotcha - Player Naked After LoadInventory() 18 | You have to use LoadEquipment() after LoadInventory(), or the player will be naked. 19 | ``` 20 | Players[pid]:LoadInventory() 21 | Players[pid]:LoadEquipment() 22 | ``` 23 | 24 | ### Temporarily Loading Cells 25 | (The following code is basically lifted from cell's base.lua) 26 | ``` 27 | local temporaryLoadedCells = {} 28 | if LoadedCells[cell] == nil then 29 | myMod.LoadCell(cell) 30 | table.insert(temporaryLoadedCells, cell) 31 | end 32 | 33 | --Then later when finished 34 | for arrayIndex, cell in pairs(temporaryLoadedCells) do 35 | myMod.UnloadCell(cell) 36 | end 37 | ``` 38 | ### Adding Items to Players' Inventories (Online/Offline) 39 | The provided example is for adding/subtracting gold, though with a bit of adaption it could be used for any item 40 | ``` 41 | --Add gold to a player's inventory, regardless of whether or not they're online. Ideally, you'd use a different function to work out if they actually have the required amount of money in their inventory first if you're allowing them to purchase stuff 42 | local function addGold(playerName, amount) --playerName is the name of the player to add the gold to (capitalization doesn't matter). Amount is the amount of gold to add (can be negative if you want to subtract gold). 43 | --Find the player 44 | local player = myMod.GetPlayerByName(playerName) 45 | 46 | --Check we found the player before proceeding 47 | if player then 48 | --Look through their inventory to find where their gold is, if they have any 49 | local goldLoc = inventoryHelper.getItemIndex(player.data.inventory, "gold_001", -1) 50 | 51 | --If they have gold in their inventory, edit that item's data. Otherwise make some new data. 52 | if goldLoc then 53 | player.data.inventory[goldLoc].count = player.data.inventory[goldLoc].count + amount 54 | 55 | --If the total is now 0 or lower, remove the entry from the player's inventory. 56 | if player.data.inventory[goldLoc].count < 1 then 57 | player.data.inventory[goldLoc] = nil 58 | end 59 | else 60 | --Only create a new entry for gold if the amount is actually above 0, otherwise we'll have negative money. 61 | if amount > 0 then 62 | table.insert(player.data.inventory, {refId = "gold_001", count = amount, charge = -1}) 63 | end 64 | end 65 | 66 | --How we save the character is different depending on whether or not the player is online 67 | if player:IsLoggedIn() then 68 | --If the player is logged in, we have to update their inventory to reflect the changes 69 | player:Save() 70 | player:LoadInventory() 71 | player:LoadEquipment() 72 | else 73 | --If the player isn't logged in, we have to temporarily set the player's logged in variable to true, otherwise the Save function won't save the player's data 74 | player.loggedIn = true 75 | player:Save() 76 | player.loggedIn = false 77 | end 78 | 79 | return true 80 | else 81 | --Couldn't find any existing player with that name 82 | return false 83 | end 84 | end 85 | ``` 86 | And as a bounus, here's how to find out how much gold the player has. Ideally if you're using a script to sell something to the player, you'd use this to first check if the player actually has enough money to afford the item before proceeding: 87 | ``` 88 | --Returns the amount of gold the player has in their inventory, regardless of whether or not they're online. Returns 0 if they have none, or false if the player couldn't be found, otherwise returns the amount of gold they have. 89 | local function getPlayerGold(playerName) --playerName is the name of the player (capitalization doesn't matter) 90 | local player = myMod.GetPlayerByName(playerName) 91 | 92 | if player then 93 | local goldLoc = inventoryHelper.getItemIndex(player.data.inventory, "gold_001", -1) 94 | 95 | if goldLoc then 96 | return player.data.inventory[goldLoc].count 97 | else 98 | return 0 99 | end 100 | else 101 | --Couldn't find the player 102 | return false 103 | end 104 | end 105 | ``` 106 | Hey lookie here! As a bonus bonus, here's an example of the original item adding script but adapted to work for any item, instead of just gold: 107 | ``` 108 | local function addItem(playerName, refId, amount, charge) --playerName is the name of the player to add the item to (capitalization doesn't matter). refId is the refId of the item. amount is the amount of item to add (can be negative if you want to subtract items). charge is optional, denoting the item's charge. 109 | --Set the charge to default, if not provided 110 | local charge = charge or -1 111 | 112 | --Find the player 113 | local player = myMod.GetPlayerByName(playerName) 114 | 115 | --Check we found the player before proceeding 116 | if player then 117 | --Look through their inventory to find where an existing instance of the item is, if they have any 118 | local itemLoc = inventoryHelper.getItemIndex(player.data.inventory, refId, charge) 119 | 120 | --If they have the item in their inventory (with a matching charge), edit that item's data. Otherwise make some new data. 121 | if itemLoc then 122 | player.data.inventory[itemLoc].count = player.data.inventory[itemLoc].count + amount 123 | 124 | --If the total is now 0 or lower, remove the entry from the player's inventory. 125 | if player.data.inventory[itemLoc].count < 1 then 126 | player.data.inventory[itemLoc] = nil 127 | end 128 | else 129 | --Only create a new entry for the item if the amount is actually above 0, otherwise we'll have negative items. 130 | if amount > 0 then 131 | table.insert(player.data.inventory, {refId = refId, count = amount, charge = charge}) 132 | end 133 | end 134 | 135 | --How we save the character is different depending on whether or not the player is online 136 | if player:IsLoggedIn() then 137 | --If the player is logged in, we have to update their inventory to reflect the changes 138 | player:Save() 139 | player:LoadInventory() 140 | player:LoadEquipment() 141 | else 142 | --If the player isn't logged in, we have to temporarily set the player's logged in variable to true, otherwise the Save function won't save the player's data 143 | player.loggedIn = true 144 | player:Save() 145 | player.loggedIn = false 146 | end 147 | 148 | return true 149 | else 150 | --Couldn't find any existing player with that name 151 | return false 152 | end 153 | end 154 | ``` 155 | 156 | ### Detecting If Item Was Spawned/Placed 157 | (Example lifted from cell's base.lua. Only the wasPlacedHere bit is actually relevant for this example.) 158 | ``` 159 | local wasPlacedHere = tableHelper.containsValue(LoadedCells[cell].data.packets.place, refIndex) or tableHelper.containsValue(LoadedCells[cell].data.packets.spawn, refIndex) 160 | 161 | LoadedCells[cell]:DeleteObjectData(refIndex) 162 | 163 | if wasPlacedHere == false then 164 | table.insert(self.data.packets.delete, refIndex) 165 | LoadedCells[cell]:InitializeObjectData(refIndex, refId) 166 | end 167 | ``` 168 | ### Deleting Objects 169 | Blah 170 | ``` 171 | local splitIndex = refIndex:split("-") 172 | 173 | for k, v in pairs(Players) do 174 | tes3mp.InitializeEvent(v.pid) 175 | tes3mp.SetEventCell(cell) 176 | tes3mp.SetObjectRefNumIndex(splitIndex[1]) 177 | tes3mp.SetObjectMpNum(splitIndex[2]) 178 | tes3mp.AddWorldObject() 179 | tes3mp.SendObjectDelete() 180 | end 181 | ``` 182 | This will only remove the object from the server. There are additional steps to remove it, depending on if the object is a data file object, or a spawned object (see: Detecting If Item Was Spawned/Placed). 183 | 184 | (Both these examples assume the cell is loaded) For spawned objects: 185 | ``` 186 | LoadedCells[cell]:DeleteObjectData(refIndex) 187 | ``` 188 | If the object is a data file object: 189 | ``` 190 | table.insert(LoadedCells[cell].data.packets.delete, refIndex) 191 | ``` 192 | After either of them, you should probably also then save the cell 193 | ``` 194 | LoadedCells[cell]:Save() 195 | ``` 196 | ### David C - Example Of How To Properly Spawn A Rat 197 | ``` 198 | local mpNum = WorldInstance:GetCurrentMpNum() + 1 199 | local cell = tes3mp.GetCell(pid) 200 | local location = { 201 | posX = tes3mp.GetPosX(pid), posY = tes3mp.GetPosY(pid), posZ = tes3mp.GetPosZ(pid), 202 | rotX = tes3mp.GetRotX(pid), rotY = 0, rotZ = tes3mp.GetRotZ(pid) 203 | } 204 | local refId = "rat" 205 | local refIndex = 0 .. "-" .. mpNum 206 | 207 | WorldInstance:SetCurrentMpNum(mpNum) 208 | tes3mp.SetCurrentMpNum(mpNum) 209 | 210 | LoadedCells[cell]:InitializeObjectData(refIndex, refId) 211 | LoadedCells[cell].data.objectData[refIndex].location = location 212 | table.insert(LoadedCells[cell].data.packets.spawn, refIndex) 213 | table.insert(LoadedCells[cell].data.packets.actorList, refIndex) 214 | LoadedCells[cell]:Save() 215 | 216 | for onlinePid, player in pairs(Players) do 217 | if player:IsLoggedIn() then 218 | tes3mp.InitializeEvent(onlinePid) 219 | tes3mp.SetEventCell(cell) 220 | tes3mp.SetObjectRefId(refId) 221 | tes3mp.SetObjectRefNumIndex(0) 222 | tes3mp.SetObjectMpNum(mpNum) 223 | tes3mp.SetObjectPosition(location.posX, location.posY, location.posZ) 224 | tes3mp.SetObjectRotation(location.rotX, location.rotY, location.rotZ) 225 | tes3mp.AddWorldObject() 226 | tes3mp.SendObjectSpawn() 227 | end 228 | end 229 | ``` 230 | ### David C, On Objects 231 | There are two important tables in a Cell's data. 232 | One is the packets table, where you just put in an object's refIndex to note that a particular object has a certain packet attached. 233 | The other is the objectData table, where the actual information required to send packets is recorded... 234 | For instance, if you're going to spawn an object, the objectData needs to contain the object's location. 235 | 236 | The JSON cell data is completely disconnected from the server's memory. 237 | i.e. If you do something like tes3mp.SetObjectPosition(), it has exactly no effect whatsoever on what is recorded in the cell data. 238 | That's why you need to both: 239 | 1) Send a packet for the players who are on the server right now 240 | and 241 | 2) Save the object in the cell's JSON data for future players 242 | As for statsDynamic, that gets populated by an authority player sending stats packets about an actor that already exists. 243 | i.e. You don't need to put that in when spawning a rat. The Lua scripts will fill it in after the rat is spawned and they get the first packet from a player about the rat's stats. 244 | 245 | ## Version 0.7\* 246 | ### Calling the functions of other scripts 247 | Quick example of how to format your script/mod/plugin/whatever the hell they're called in 0.7 (henceforth referred to as a scriptamajig): 248 | ``` 249 | function GetValue() 250 | -- Whatever 251 | end 252 | ``` 253 | Then somewhere after all the functions you want to give access to are declared (actually, not sure if it *has* to be after them if they're global...), ideally at the very bottom of the script: 254 | ``` 255 | Data["MyAmazingMod"] = {} 256 | Data.MyAmazingMod["GetValue"] = GetValue 257 | ``` 258 | Obviously change "MyAmazingMod" to your scriptamajig's name (technically you could change it to anything, but it's good practice to use your scriptamajig's name). Then in the other scriptamajig, you can call the function via: 259 | ``` 260 | Data.MyAmazingMod.GetValue() 261 | ``` 262 | -------------------------------------------------------------------------------- /bannedEquipment.lua: -------------------------------------------------------------------------------- 1 | --bannedEquipment - Release 2.1 - For tes3mp v0.6.1 2 | --Disallow select items from being equipped by players. 3 | 4 | local Methods = {} 5 | 6 | --[[ INSTALLATION 7 | 1) Save as bannedEquipment.lua in the scripts folder 8 | 2) Add [ bannedEquipment = require("bannedEquipment") ] to the top of server.lua 9 | 3) Add the following line to OnPlayerEquipment in server.lua 10 | [ bannedEquipment.OnPlayerEquipment(pid) ] 11 | ]] 12 | color = require("color") 13 | 14 | --Add item ref of items in the style of the example. RefIds should be in all lowercase. 15 | local equipBanList = {} 16 | --equipBanList["daedric dagger"] = true 17 | 18 | Methods.OnPlayerEquipment = function(pid) 19 | local changes = false 20 | for slotId, itemData in pairs(Players[pid].data.equipment) do 21 | if equipBanList[itemData.refId] then 22 | Players[pid].data.equipment[slotId] = nil 23 | Players[pid]:LoadInventory() 24 | Players[pid]:LoadEquipment() 25 | changes = true 26 | end 27 | end 28 | if changes then 29 | tes3mp.SendMessage(pid, color.Warning .. "Banned equipment has been unequipped.\n" ..color.Default, false) 30 | --This hack updates the player's inventory if it's open. There may be a better way of doing this. 31 | tes3mp.SendCell(pid) 32 | tes3mp.SendPos(pid) 33 | end 34 | end 35 | 36 | return Methods 37 | -------------------------------------------------------------------------------- /classCap.lua: -------------------------------------------------------------------------------- 1 | -- classCap - Release 1.1 - For tes3mp v0.6.1. Requires classInfo 2 | -- Caps Major, Minor, and Misc skills to certain amounts based on class. 3 | -- Not fully tested 4 | 5 | --[[ INSTALLATION 6 | 1) Save this file as "classCap.lua" in mp-stuff/scripts 7 | 2) Add [ classCap = require("classCap") ] to the top of server.lua 8 | 3) Add the following to OnPlayerSkill in server.lua 9 | [ classCap.OnPlayerSkill(pid) ] 10 | ]] 11 | 12 | local Methods = {} 13 | 14 | classInfo = require("classInfo") 15 | 16 | local config = {} 17 | 18 | --Skill levels to cap the skills at per category 19 | config.MajorCap = 100 20 | config.MinorCap = 75 21 | config.MiscCap = 50 22 | 23 | config.disableLevelProgress = true -- Whether to remove the levelup progress that comes from levelling a major or minor skill. 24 | config.disableAttributeGain = true -- Whether to remove the attribute bonus that would come from increasing the skill. 25 | -- Note: Since the script will always assume that skills were naturally levelled rather than being set by a script, it'll always detract the difference from the player's level progress/attribute gains. 26 | 27 | 28 | --Go through class' major and minor skills. If the skill id doesn't appear, that means it's a misc skill 29 | local function isMisc(skillId, majors, minors) 30 | --Check major skills 31 | for index, skill in pairs(majors) do 32 | if skill == skillId then 33 | return false 34 | end 35 | end 36 | 37 | --Check minor skills 38 | for index, skill in pairs(minors) do 39 | if skill == skillId then 40 | return false 41 | end 42 | end 43 | 44 | --It wasn't a major or a minor skill, therefore it's a misc skill 45 | return true 46 | end 47 | 48 | local function doTheThing(pid) 49 | local pClass = classInfo.GetPlayerClassData(pid) 50 | 51 | local changes = {} 52 | 53 | for index, skill in pairs(pClass.majorSkills) do 54 | local slevel = tes3mp.GetSkillBase(pid, skill) 55 | if slevel > config.MajorCap then 56 | tes3mp.SetSkillBase(pid, skill, config.MajorCap) 57 | table.insert(changes, tes3mp.GetSkillName(skill)) 58 | 59 | if config.disableLevelProgress then 60 | local penalty = slevel - config.MajorCap 61 | tes3mp.SetLevelProgress(pid, math.max((tes3mp.GetLevelProgress(pid) - penalty), 0)) --Set the player's level progress to their current minus the penalty, or 0 if it goes beneath that 62 | end 63 | 64 | if config.disableAttributeGain then 65 | local penalty = slevel - config.MajorCap 66 | local attribute = classInfo.GetGovernedAttribute(skill) 67 | 68 | tes3mp.SetSkillIncrease(pid, attribute, math.max((tes3mp.GetSkillIncrease(pid, attribute) - penalty), 0)) 69 | end 70 | end 71 | end 72 | 73 | for index, skill in pairs(pClass.minorSkills) do 74 | local slevel = tes3mp.GetSkillBase(pid, skill) 75 | if slevel > config.MinorCap then 76 | tes3mp.SetSkillBase(pid, skill, config.MinorCap) 77 | table.insert(changes, tes3mp.GetSkillName(skill)) 78 | 79 | if config.disableLevelProgress then 80 | local penalty = slevel - config.MinorCap 81 | tes3mp.SetLevelProgress(pid, math.max((tes3mp.GetLevelProgress(pid) - penalty), 0)) --Set the player's level progress to their current minus the penalty, or 0 if it goes beneath that 82 | end 83 | 84 | if config.disableAttributeGain then 85 | local penalty = slevel - config.MinorCap 86 | local attribute = classInfo.GetGovernedAttribute(skill) 87 | 88 | tes3mp.SetSkillIncrease(pid, attribute, math.max((tes3mp.GetSkillIncrease(pid, attribute) - penalty), 0)) 89 | end 90 | end 91 | end 92 | 93 | --Class info doesn't have a way of detecting misc skills at the moment, so we'll just do something a little bit hacky :P 94 | --We go through every skill id... 95 | for skill = 0, 26 do 96 | --...and check that it isn't a major or minor skill... 97 | if isMisc(skill, pClass.majorSkills, pClass.minorSkills) then 98 | --... if it isn't, then we treat it as a misc skill 99 | local slevel = tes3mp.GetSkillBase(pid, skill) 100 | if slevel > config.MiscCap then 101 | tes3mp.SetSkillBase(pid, skill, config.MiscCap) 102 | table.insert(changes, tes3mp.GetSkillName(skill)) 103 | 104 | if config.disableAttributeGain then 105 | local penalty = slevel - config.MiscCap 106 | local attribute = classInfo.GetGovernedAttribute(skill) 107 | 108 | tes3mp.SetSkillIncrease(pid, attribute, math.max((tes3mp.GetSkillIncrease(pid, attribute) - penalty), 0)) 109 | end 110 | end 111 | end 112 | end 113 | 114 | --Only do anything if there were changes made 115 | if #changes > 0 then 116 | --Do some nonsense to make a pretty message 117 | local message = "You've hit the cap for your " 118 | 119 | for i=1, #changes do 120 | --Add the skill name to the list 121 | message = message .. changes[i] 122 | 123 | if changes[i+1] ~= nil and changes[i+2] == nil then -- If the next entry exists but the one after it doesn't, that means this is the penultimate entry 124 | message = message .. " and " 125 | elseif changes[i+1] == nil then --There's no entry after this one. 126 | --Do nothing 127 | else --It's none of the above, so just stick a comma on the end 128 | message = message .. ", " 129 | end 130 | end 131 | 132 | if #changes > 1 then 133 | message = message .. " skills.\n" 134 | else 135 | message = message .. " skill.\n" 136 | end 137 | 138 | tes3mp.SendMessage(pid, color.Warning .. message .. color.Default, false) 139 | 140 | Players[pid]:SaveSkills() 141 | Players[pid]:LoadSkills() 142 | end 143 | end 144 | 145 | Methods.OnPlayerSkill = function(pid) 146 | --On player skill gets called when the player joins the server but isn't actually logged in. 147 | if Players[pid]:IsLoggedIn() then 148 | doTheThing(pid) 149 | end 150 | end 151 | 152 | return Methods 153 | -------------------------------------------------------------------------------- /classInfo.lua: -------------------------------------------------------------------------------- 1 | -- classInfo - Release 1.1 - For tes3mp v0.6.1 2 | -- save as classInfo.lua in the scripts folder 3 | 4 | --[[ INFO 5 | SPECIALIZATIONS: 0 Combat | 1 Magic | 2 Stealth 6 | ATTRIBUTES: 0 Strength | 1 Intelligence | 2 Willpower | 3 Agility | 4 Speed | 5 Endurance | 6 Personality 7 | SKILLS: 8 | 0 Block | 1 Armorer | 2 MediumArmor | 3 HeavyArmor | 4 BluntWeapon | 5 LongBlade | 6 Axe | 7 Spear | 8 Athletics 9 | 9 Enchant | 10 Destruction | 11 Alteration | 12 Illusion | 13 Conjuration | 14 Mysticism | 15 Restoration | 16 Alchemy | 17 Unarmored 10 | 18 Security | 19 Sneak | 20 Acrobatics | 21 LightArmor | 22 ShortBlade | 23 Marksman | 24 Mercantile | 25 Speechcraft | 26 HandToHand 11 | ]] 12 | 13 | --[[ STRUCTURE/TEMPLATE 14 | classList["lowercasename"] = { 15 | name = "Name", 16 | specialization = specializationNum, 17 | majorAttributes = {attributeid, attributeid}, 18 | majorSkills = {skillid, skillid, skillid, skillid, skillid}, 19 | minorSkills = {skillid, skillid, skillid, skillid, skillid}, 20 | description = "description", 21 | } 22 | ]] 23 | 24 | local Methods = {} 25 | 26 | local classList = {} 27 | 28 | classList["acrobat"] = { 29 | name = "Acrobat", 30 | specialization = 2, 31 | majorAttributes = {3, 5}, 32 | majorSkills = {20, 8, 23, 19, 17}, 33 | minorSkills = {25, 11, 7, 26, 21}, 34 | description = "Acrobat is a polite euphemism for agile burglars and second-story men. These thieves avoid detection by stealth, and rely on mobility and cunning to avoid capture.", 35 | } 36 | classList["agent"] = { 37 | name = "Agent", 38 | specialization = 2, 39 | majorAttributes = {6, 3}, 40 | majorSkills = {25, 19, 20, 21, 22}, 41 | minorSkills = {24, 13, 0, 17, 12}, 42 | description = "Agents are operatives skilled in deception and avoidance, but trained in self-defense and the use of deadly force. Self-reliant and independent, agents devote themselves to personal goals, or to various patrons or causes.", 43 | } 44 | classList["archer"] = { 45 | name = "Archer", 46 | specialization = 0, 47 | majorAttributes = {3, 0}, 48 | majorSkills = {23, 5, 0, 8, 21}, 49 | minorSkills = {17, 7, 15, 19, 2}, 50 | description = "Archers are fighters specializing in long-range combat and rapid movement. Opponents are kept at distance by ranged weapons and swift maneuver, and engaged in melee with sword and shield after the enemy is wounded and weary.", 51 | } 52 | classList["assassin"] = { 53 | name = "Assassin", 54 | specialization = 2, 55 | majorAttributes = {4, 1}, 56 | majorSkills = {19, 23, 21, 22, 20}, 57 | minorSkills = {18, 5, 16, 0, 8}, 58 | description = "Assassins are killers who rely on stealth and mobility to approach victims undetected. Execution is with ranged weapons or with short blades for close work. Assassins include ruthless murderers and principled agents of noble causes.", 59 | } 60 | classList["barbarian"] = { 61 | name = "Barbarian", 62 | specialization = 0, 63 | majorAttributes = {0, 4}, 64 | majorSkills = {6, 2, 4, 8, 0}, 65 | minorSkills = {20, 21, 1, 23, 17}, 66 | description = "Barbarians are the proud, savage warrior elite of the plains nomads, mountain tribes, and sea reavers. They tend to be brutal and direct, lacking civilized graces, but they glory in heroic feats, and excel in fierce, frenzied single combat.", 67 | } 68 | classList["bard"] = { 69 | name = "Bard", 70 | specialization = 2, 71 | majorAttributes = {6, 1}, 72 | majorSkills = {25, 8, 20, 5, 0}, 73 | minorSkills = {24, 12, 2, 9, 18}, 74 | description = "Bards are loremasters and storytellers. They crave adventure for the wisdom and insight to be gained, and must depend on sword, shield, spell and enchantment to preserve them from the perils of their educational experiences.", 75 | } 76 | classList["battlemage"] = { 77 | name = "Battlemage", 78 | specialization = 1, 79 | majorAttributes = {1, 0}, 80 | majorSkills = {11, 10, 13, 6, 3}, 81 | minorSkills = {14, 5, 23, 9, 16}, 82 | description = "Battlemages are wizard-warriors, trained in both lethal spellcasting and heavily armored combat. They sacrifice mobility and versatility for the ability to supplement melee and ranged attacks with elemental damage and summoned creatures.", 83 | } 84 | classList["crusader"] = { 85 | name = "Crusader", 86 | specialization = 0, 87 | majorAttributes = {3, 0}, 88 | majorSkills = {4, 5, 10, 3, 0}, 89 | minorSkills = {15, 1, 26, 2, 16}, 90 | description = "Any heavily armored warrior with spellcasting powers and a good cause may call himself a Crusader. Crusaders do well by doing good. They hunt monsters and villains, making themselves rich by plunder as they rid the world of evil.", 91 | } 92 | classList["healer"] = { 93 | name = "Healer", 94 | specialization = 1, 95 | majorAttributes = {2, 6}, 96 | majorSkills = {15, 14, 11, 26, 25}, 97 | minorSkills = {12, 16, 17, 21, 4}, 98 | description = "Healers are spellcasters who swear solemn oaths to heal the afflicted and cure the diseased. When threatened, they defend themselves with reason and disabling attacks and magic, relying on deadly force only in extremity.", 99 | } 100 | classList["knight"] = { 101 | name = "Knight", 102 | specialization = 0, 103 | majorAttributes = {0, 6}, 104 | majorSkills = {5, 6, 25, 3, 0}, 105 | minorSkills = {15, 24, 2, 9, 1}, 106 | description = "Of noble birth, or distinguished in battle or tourney, knights are civilized warriors, schooled in letters and courtesy, governed by the codes of chivalry. In addition to the arts of war, knights study the lore of healing and enchantment.", 107 | } 108 | classList["mage"] = { 109 | name = "Mage", 110 | specialization = 1, 111 | majorAttributes = {1, 2}, 112 | majorSkills = {14, 10, 11, 12, 15}, 113 | minorSkills = {9, 16, 17, 22, 13}, 114 | description = "Most mages claim to study magic for its intellectual rewards, but they also often profit from its practical applications. Varying widely in temperament and motivation, mages share but one thing in common - an avid love of spellcasting.", 115 | } 116 | classList["monk"] = { 117 | name = "Monk", 118 | specialization = 2, 119 | majorAttributes = {3, 2}, 120 | majorSkills = {26, 17, 8, 20, 19}, 121 | minorSkills = {0, 23, 21, 15, 4}, 122 | description = "Monks are students of the ancient martial arts of hand-to-hand combat and unarmored self defense. Monks avoid detection by stealth, mobility, and Agility, and are skilled with a variety of ranged and close-combat weapons.", 123 | } 124 | classList["nightblade"] = { 125 | name = "Nightblade", 126 | specialization = 1, 127 | majorAttributes = {2, 4}, 128 | majorSkills = {14, 12, 11, 19, 22}, 129 | minorSkills = {21, 17, 10, 23, 18}, 130 | description = "Nightblades are spellcasters who use their magics to enhance mobility, concealment, and stealthy close combat. They have a sinister reputation, since many nightblades are thieves, enforcers, assassins, or covert agents.", 131 | } 132 | classList["pilgrim"] = { 133 | name = "Pilgrim", 134 | specialization = 2, 135 | majorAttributes = {6, 5}, 136 | majorSkills = {25, 24, 23, 15, 2}, 137 | minorSkills = {12, 26, 22, 0, 16}, 138 | description = "Pilgrims are travellers, seekers of truth and enlightenment. They fortify themselves for road and wilderness with arms, armor, and magic, and through wide experience of the world, they become shrewd in commerce and persuasion.", 139 | } 140 | classList["rogue"] = { 141 | name = "Rogue", 142 | specialization = 0, 143 | majorAttributes = {4, 6}, 144 | majorSkills = {22, 24, 6, 21, 26}, 145 | minorSkills = {0, 2, 25, 8, 5}, 146 | description = "Rogues are adventurers and opportunists with a gift for getting in and out of trouble. Relying variously on charm and dash, blades and business sense, they thrive on conflict and misfortune, trusting to their luck and cunning to survive.", 147 | } 148 | classList["scout"] = { 149 | name = "Scout", 150 | specialization = 0, 151 | majorAttributes = {4, 5}, 152 | majorSkills = {19, 5, 2, 8, 0}, 153 | minorSkills = {23, 16, 11, 21, 17}, 154 | description = "Scouts rely on stealth to survey routes and opponents, using ranged weapons and skirmish tactics when forced to fight. By contrast with barbarians, in combat scouts tend to be cautious and methodical, rather than impulsive.", 155 | } 156 | classList["sorcerer"] = { 157 | name = "Sorcerer", 158 | specialization = 1, 159 | majorAttributes = {1, 5}, 160 | majorSkills = {9, 13, 14, 10, 11}, 161 | minorSkills = {12, 2, 3, 23, 22}, 162 | description = "Though spellcasters by vocation, sorcerers rely most on summonings and enchantments. They are greedy for magic scrolls, rings, armor and weapons, and commanding undead and Daedric servants gratifies their egos.", 163 | } 164 | classList["spellsword"] = { 165 | name = "Spellsword", 166 | specialization = 1, 167 | majorAttributes = {2, 5}, 168 | majorSkills = {0, 15, 5, 10, 11}, 169 | minorSkills = {4, 9, 16, 2, 6}, 170 | description = "Spellswords are spellcasting specialists trained to support Imperial troops in skirmish and in battle. Veteran spellswords are prized as mercenaries, and well-suited for careers as adventurers and soldiers-of-fortune.", 171 | } 172 | classList["thief"] = { 173 | name = "Thief", 174 | specialization = 2, 175 | majorAttributes = {4, 3}, 176 | majorSkills = {18, 19, 20, 21, 22}, 177 | minorSkills = {23, 25, 26, 24, 8}, 178 | description = "Thieves are pickpockets and pilferers. Unlike robbers, who kill and loot, thieves typically choose stealth and subterfuge over violence, and often entertain romantic notions of their charm and cleverness in their acquisitive activities.", 179 | } 180 | classList["warrior"] = { 181 | name = "Warrior", 182 | specialization = 0, 183 | majorAttributes = {0, 5}, 184 | majorSkills = {5, 2, 3, 8, 0}, 185 | minorSkills = {1, 7, 23, 6, 4}, 186 | description = "Warriors are the professional men-at-arms, soldiers, mercenaries, and adventurers of the Empire, trained with various weapons and armor styles, conditioned by long marches, and hardened by ambush, skirmish, and battle.", 187 | } 188 | classList["witchhunter"] = { 189 | name = "Witchhunter", 190 | specialization = 1, 191 | majorAttributes = {1, 3}, 192 | majorSkills = {13, 9, 16, 21, 23}, 193 | minorSkills = {17, 0, 4, 19, 14}, 194 | description = "Witchhunters are dedicated to rooting out and destroying the perverted practices of dark cults and profane sorcery. They train for martial, magical, and stealthy war against vampires, witches, warlocks, and necromancers.", 195 | } 196 | 197 | local governedAttributes = { 198 | --Skill id = attribute id 199 | [0] = 3, -- Block = Agility 200 | [1] = 0, -- Armorer = Strength 201 | [2] = 5, -- Medium Armor = Endurance 202 | [3] = 5, -- Heavy Armor = Endurance 203 | [4] = 0, -- Blunt Weapon = Strength 204 | [5] = 0, -- Long Blade = Strength 205 | [6] = 0, -- Axe = Strength 206 | [7] = 5, -- Spear = Endurance 207 | [8] = 4, -- Athletics = Speed 208 | [9] = 1, -- Enchant = Intelligence 209 | [10] = 2, -- Destruction = Willpower 210 | [11] = 2, -- Alteration = Willpower 211 | [12] = 6, -- Illusion = Personality 212 | [13] = 1, -- Conjuration = Intelligence 213 | [14] = 2, -- Mysticism = Willpower 214 | [15] = 2, -- Restoration = Willpower 215 | [16] = 1, -- Alchemy = Intelligence 216 | [17] = 4, -- Unarmored = Speed 217 | [18] = 1, -- Security = Intelligence 218 | [19] = 3, -- Sneak = Agility 219 | [20] = 0, -- Acrobatics = Strength 220 | [21] = 3, -- Light Armor = Agility 221 | [22] = 4, -- Short Blade = Speed 222 | [23] = 3, -- Marksman = Agility 223 | [24] = 6, -- Mercantile = Personality 224 | [25] = 6, -- Speechcraft = Personality 225 | [26] = 4, -- Hand-to-hand = Speed 226 | } 227 | 228 | 229 | -- ==FUNCTIONS== 230 | local function makeCustom(pid) 231 | local pClass = Players[pid].data.customClass 232 | local out = {} 233 | 234 | out.name = pClass.name 235 | out.description = pClass.description 236 | out.specialization = pClass.specialization 237 | 238 | local attributes = {} 239 | table.insert(attributes, tes3mp.GetClassMajorAttribute(pid, 0)) 240 | table.insert(attributes, tes3mp.GetClassMajorAttribute(pid, 1)) 241 | 242 | local major = {} 243 | table.insert(major, tes3mp.GetClassMajorSkill(pid, 0)) 244 | table.insert(major, tes3mp.GetClassMajorSkill(pid, 1)) 245 | table.insert(major, tes3mp.GetClassMajorSkill(pid, 2)) 246 | table.insert(major, tes3mp.GetClassMajorSkill(pid, 3)) 247 | table.insert(major, tes3mp.GetClassMajorSkill(pid, 4)) 248 | 249 | local minor = {} 250 | table.insert(minor, tes3mp.GetClassMinorSkill(pid, 0)) 251 | table.insert(minor, tes3mp.GetClassMinorSkill(pid, 1)) 252 | table.insert(minor, tes3mp.GetClassMinorSkill(pid, 2)) 253 | table.insert(minor, tes3mp.GetClassMinorSkill(pid, 3)) 254 | table.insert(minor, tes3mp.GetClassMinorSkill(pid, 4)) 255 | 256 | out.majorAttributes = attributes 257 | out.majorSkills = major 258 | out.minorSkills = minor 259 | 260 | return out 261 | end 262 | 263 | local function getDefaultClass(classId) 264 | return classList[classId] 265 | end 266 | 267 | local function addClass(classData) 268 | classList[string.lower(classData.name)] = classData 269 | end 270 | 271 | local function getPlayerClass(pid) 272 | if tes3mp.IsClassDefault(pid) == 1 then 273 | return getDefaultClass(Players[pid].data.character.class) 274 | else 275 | return makeCustom(pid) 276 | end 277 | end 278 | 279 | local function getGovernedAttribute(skillId) 280 | return governedAttributes[skillId] 281 | end 282 | 283 | local specializations = {[0] = "Combat", [1] = "Magic", [2] = "Stealth"} 284 | local function getSpecializationName(specializationId) 285 | return specializations[specializationId] 286 | end 287 | 288 | -- ==METHODS== 289 | --[[ Useful methods from the base mod: 290 | tes3mp.GetAttributeName(attributeId) 291 | tes3mp.GetSkillName(skillId) 292 | 293 | tes3mp.GetAttributeId(attributeName) 294 | tes3mp.GetSkillId(skillName) 295 | ]] 296 | 297 | -- METHODS THAT TAKE PIDS 298 | -- Use to get the data on a player's class (custom or default), given their pid. 299 | Methods.GetPlayerClassData = function(pid) 300 | return getPlayerClass(pid) 301 | end 302 | 303 | --Use to make a table of class information based off a player's custom class that's compatible with this script's methods. Usually done automatically if you use GetPlayerClassData on a player with a custom class. 304 | Methods.GetCustomClassData = function(pid) 305 | return makeCustom(pid) 306 | end 307 | 308 | -- METHODS THAT TAKE CLASS DATA 309 | -- Most of these don't necessarily need to be used since they can just be accessed straight from the class data 310 | Methods.GetClassName = function(classData) 311 | return classData.name 312 | end 313 | Methods.GetClassSpecialization = function(classData) 314 | return classData.specialization 315 | end 316 | Methods.GetClassDescription = function(classData) 317 | return classData.description 318 | end 319 | Methods.GetClassMajorAttributes = function(classData) 320 | return classData.majorAttributes 321 | end 322 | Methods.GetClassMajorSkills = function(classData) 323 | return classData.majorSkills 324 | end 325 | Methods.GetClassMinorSkills = function(classData) 326 | return classData.minorSkills 327 | end 328 | 329 | -- METHODS THAT TAKE OTHER STUFF 330 | -- Use to get the data on a default class given the class name 331 | Methods.GetClassData = function(className) 332 | return getDefaultClass(string.lower(className)) 333 | end 334 | 335 | -- Use to remotely add new class information to the classInfo table. The data should be a table formatted as the classes are in this script. A lowercase version of the name entry will be used as the key. It's unadvised to add players' custom classes to the list, as the custom names can overwrite existing entries. Additions to the classInfo table aren't saved and have to be redone every server launch. 336 | Methods.AddClass = function(classData) 337 | addClass(classData) 338 | end 339 | 340 | Methods.GetSpecializationName = function(specializationId) 341 | return getSpecializationName(specializationId) 342 | end 343 | 344 | -- Takes a provided Skill ID and returns the Attribute ID of the attribute the skill is governed by. Not technically anything to do with classes - consider it a bonus function :P 345 | Methods.GetGovernedAttribute = function(skillId) 346 | return getGovernedAttribute(skillId) 347 | end 348 | 349 | 350 | return Methods 351 | -------------------------------------------------------------------------------- /decorateHelp.lua: -------------------------------------------------------------------------------- 1 | -- decorateHelp - Release 1.1 - For tes3mp v0.6.1 2 | -- Alter positions of items using a GUI 3 | 4 | --[[ INSTALLATION: 5 | 1) Save this file as "decorateHelp.lua" in mp-stuff/scripts 6 | 2) Add [ decorateHelp = require("decorateHelp") ] to the top of server.lua 7 | 3) Add the following to the elseif chain for commands in "OnPlayerSendMessage" inside server.lua 8 | 9 | [ elseif cmd[1] == "decorator" or cmd[1] == "decorate" or cmd[1] == "dh" then 10 | decorateHelp.OnCommand(pid) ] 11 | 4) Add the following to OnGUIAction in server.lua 12 | [ if decorateHelp.OnGUIAction(pid, idGui, data) then return end ] 13 | 5) Add the following to OnObjectPlace in server.lua 14 | [ decorateHelp.OnObjectPlace(pid, cellDescription) ] 15 | 6) Add the following to OnPlayerCellChange in server.lua 16 | [ decorateHelp.OnPlayerCellChange(pid) ] 17 | 18 | ]] 19 | 20 | ------ 21 | local config = {} 22 | 23 | config.MainId = 31360 24 | config.PromptId = 31361 25 | ------ 26 | 27 | local Methods = {} 28 | 29 | tableHelper = require("tableHelper") 30 | 31 | -- 32 | local playerSelectedObject = {} 33 | local playerCurrentMode = {} 34 | 35 | --Returns the object's data from a loaded cell. Doesn't need to load the cell because this assumes it'll always be called in a cell that's loaded. 36 | local function getObject(refIndex, cell) 37 | if refIndex == nil then 38 | return false 39 | end 40 | 41 | if LoadedCells[cell]:ContainsObject(refIndex) then 42 | return LoadedCells[cell].data.objectData[refIndex] 43 | else 44 | return false 45 | end 46 | end 47 | 48 | local function resendPlaceToAll(refIndex, cell) 49 | local object = getObject(refIndex, cell) 50 | 51 | if not object then 52 | return false 53 | end 54 | 55 | local refId = object.refId 56 | local count = object.count or 1 57 | local charge = object.charge or -1 58 | local posX, posY, posZ = object.location.posX, object.location.posY, object.location.posZ 59 | local rotX, rotY, rotZ = object.location.rotX, object.location.rotY, object.location.rotZ 60 | local refIndex = refIndex 61 | 62 | local inventory = object.inventory or nil 63 | 64 | local splitIndex = refIndex:split("-") 65 | 66 | for pid, pdata in pairs(Players) do 67 | if Players[pid]:IsLoggedIn() then 68 | --First, delete the original 69 | tes3mp.InitializeEvent(pid) 70 | tes3mp.SetEventCell(cell) 71 | tes3mp.SetObjectRefNumIndex(0) 72 | tes3mp.SetObjectMpNum(splitIndex[2]) 73 | tes3mp.AddWorldObject() --? 74 | tes3mp.SendObjectDelete() 75 | 76 | --Now remake it 77 | tes3mp.InitializeEvent(pid) 78 | tes3mp.SetEventCell(cell) 79 | tes3mp.SetObjectRefId(refId) 80 | tes3mp.SetObjectCount(count) 81 | tes3mp.SetObjectCharge(charge) 82 | tes3mp.SetObjectPosition(posX, posY, posZ) 83 | tes3mp.SetObjectRotation(rotX, rotY, rotZ) 84 | tes3mp.SetObjectRefNumIndex(0) 85 | tes3mp.SetObjectMpNum(splitIndex[2]) 86 | if inventory then 87 | for itemIndex, item in pairs(inventory) do 88 | tes3mp.SetContainerItemRefId(item.refId) 89 | tes3mp.SetContainerItemCount(item.count) 90 | tes3mp.SetContainerItemCharge(item.charge) 91 | 92 | tes3mp.AddContainerItem() 93 | end 94 | end 95 | 96 | tes3mp.AddWorldObject() 97 | tes3mp.SendObjectPlace() 98 | if inventory then 99 | tes3mp.SendContainer() 100 | end 101 | end 102 | end 103 | 104 | LoadedCells[cell]:Save() --Not needed, but it's nice to do anyways 105 | end 106 | 107 | 108 | local function showPromptGUI(pid) 109 | local message = "[" .. playerCurrentMode[tes3mp.GetName(pid)] .. "] - Enter a number to add/subtract." 110 | 111 | tes3mp.InputDialog(pid, config.PromptId, message) 112 | end 113 | 114 | local function onEnterPrompt(pid, data) 115 | local cell = tes3mp.GetCell(pid) 116 | local pname = tes3mp.GetName(pid) 117 | local mode = playerCurrentMode[pname] 118 | local data = tonumber(data) or 0 119 | 120 | local object = getObject(playerSelectedObject[pname], cell) 121 | 122 | if not object then 123 | --The object no longer exists, so we should bail out now 124 | return false 125 | end 126 | 127 | if mode == "Rotate X" then 128 | local curDegrees = math.deg(object.location.rotX) 129 | local newDegrees = (curDegrees + data) % 360 130 | object.location.rotX = math.rad(newDegrees) 131 | elseif mode == "Rotate Y" then 132 | local curDegrees = math.deg(object.location.rotY) 133 | local newDegrees = (curDegrees + data) % 360 134 | object.location.rotY = math.rad(newDegrees) 135 | elseif mode == "Rotate Z" then 136 | local curDegrees = math.deg(object.location.rotZ) 137 | local newDegrees = (curDegrees + data) % 360 138 | object.location.rotZ = math.rad(newDegrees) 139 | elseif mode == "Move North" then 140 | object.location.posY = object.location.posY + data 141 | elseif mode == "Move East" then 142 | object.location.posX = object.location.posX + data 143 | elseif mode == "Move Up" then 144 | object.location.posZ = object.location.posZ + data 145 | elseif mode == "Scale Up" then 146 | --Not sure how scale works atm. 147 | --TODO 148 | return 149 | end 150 | 151 | resendPlaceToAll(playerSelectedObject[pname], cell) 152 | end 153 | 154 | local function showMainGUI(pid) 155 | --Determine if the player has an item 156 | local currentItem = "None" --default 157 | local selected = playerSelectedObject[tes3mp.GetName(pid)] 158 | local object = getObject(selected, tes3mp.GetCell(pid)) 159 | 160 | if selected and object then --If they have an entry and it isn't gone 161 | currentItem = object.refId .. " (" .. selected .. ")" 162 | end 163 | 164 | local message = "Select an option. Your current item: " .. currentItem 165 | tes3mp.CustomMessageBox(pid, config.MainId, message, "Move North;Move East;Move Up;Rotate X;Rotate Y;Rotate Z;Scale Up;Grab;Close") 166 | end 167 | 168 | local function setSelectedObject(pid, refIndex) 169 | playerSelectedObject[tes3mp.GetName(pid)] = refIndex 170 | end 171 | 172 | Methods.SetSelectedObject = function(pid, refIndex) 173 | setSelectedObject(pid, refIndex) 174 | end 175 | 176 | Methods.OnObjectPlace = function(pid, cellDescription) 177 | --Get the last event, which should hopefully be the place packet 178 | tes3mp.ReadLastEvent() 179 | 180 | --Get the refIndex of the first item in the object place packet (in theory, there should only by one) 181 | local refIndex = tes3mp.GetObjectRefNumIndex(0) .. "-" .. tes3mp.GetObjectMpNum(0) 182 | 183 | --Record that item as the last one the player interacted with in this cell 184 | setSelectedObject(pid, refIndex) 185 | end 186 | 187 | Methods.OnGUIAction = function(pid, idGui, data) 188 | local pname = tes3mp.GetName(pid) 189 | 190 | if idGui == config.MainId then 191 | if tonumber(data) == 0 then --Move North 192 | playerCurrentMode[pname] = "Move North" 193 | showPromptGUI(pid) 194 | return true 195 | elseif tonumber(data) == 1 then --Move East 196 | playerCurrentMode[pname] = "Move East" 197 | showPromptGUI(pid) 198 | return true 199 | elseif tonumber(data) == 2 then --Move Up 200 | playerCurrentMode[pname] = "Move Up" 201 | showPromptGUI(pid) 202 | return true 203 | elseif tonumber(data) == 3 then --Rotate X 204 | playerCurrentMode[pname] = "Rotate X" 205 | showPromptGUI(pid) 206 | return true 207 | elseif tonumber(data) == 4 then --Rotate Y 208 | playerCurrentMode[pname] = "Rotate Y" 209 | showPromptGUI(pid) 210 | return true 211 | elseif tonumber(data) == 5 then --Rotate Z 212 | playerCurrentMode[pname] = "Rotate Z" 213 | showPromptGUI(pid) 214 | return true 215 | elseif tonumber(data) == 6 then --Scale Up 216 | --TODO 217 | tes3mp.MessageBox(pid, -1, "Not yet implemented, sorry.") 218 | return true 219 | elseif tonumber(data) == 7 then --Grab 220 | --TODO 221 | tes3mp.MessageBox(pid, -1, "Not yet implemented, sorry.") 222 | return true 223 | elseif tonumber(data) == 8 then --Close 224 | --Do nothing 225 | return true 226 | end 227 | elseif idGui == config.PromptId then 228 | if data ~= nil and data ~= "" and tonumber(data) then 229 | onEnterPrompt(pid, data) 230 | end 231 | 232 | playerCurrentMode[tes3mp.GetName(pid)] = nil 233 | return true, showMainGUI(pid) 234 | end 235 | end 236 | 237 | Methods.OnPlayerCellChange = function(pid) 238 | playerSelectedObject[tes3mp.GetName(pid)] = nil 239 | end 240 | 241 | Methods.OnCommand = function(pid) 242 | showMainGUI(pid) 243 | end 244 | 245 | return Methods 246 | -------------------------------------------------------------------------------- /decoratorsAid.lua: -------------------------------------------------------------------------------- 1 | -- decoratorsAid - Release 1 - For tes3mp v0.6.1 2 | 3 | --[[ INSTALLATION: 4 | 1) Save this file as "decoratorsAid.lua" in mp-stuff/scripts 5 | 2) Add [ decoratorsAid = require("decoratorsAid") ] to the top of server.lua 6 | 3) Add the following to the elseif chain for commands in "OnPlayerSendMessage" inside server.lua 7 | 8 | [ elseif cmd[1] == "decorator" or cmd[1] == "decorate" or cmd[1] == "dh" then 9 | decoratorsAid.OnCommand(pid) ] 10 | 4) Add the following to OnGUIAction in server.lua 11 | [ if decoratorsAid.OnGUIAction(pid, idGui, data) then return end ] 12 | 5) Add the following to OnObjectPlace in server.lua 13 | [ decoratorsAid.OnObjectPlace(pid, cellDescription) ] 14 | 6) Add the following to OnPlayerCellChange in server.lua 15 | [ decoratorsAid.OnPlayerCellChange(pid) ] 16 | 17 | ]] 18 | 19 | ------ 20 | local config = {} 21 | 22 | config.MainId = 31350 23 | config.PromptId = 31351 24 | ------ 25 | 26 | Methods = {} 27 | 28 | tableHelper = require("tableHelper") 29 | 30 | -- 31 | local playerLastObject = {} 32 | local playerCurrentMode = {} 33 | 34 | --Returns the object's data from a loaded cell. Doesn't need to load the cell because this assumes it'll always be called in a cell that's loaded. 35 | local function getObject(refIndex, cell) 36 | if refIndex == nil then 37 | return false 38 | end 39 | 40 | if LoadedCells[cell]:ContainsObject(refIndex) then 41 | return LoadedCells[cell].data.objectData[refIndex] 42 | else 43 | return false 44 | end 45 | end 46 | 47 | local function resendPlace(pid, refIndex, cell) 48 | local object = getObject(refIndex, cell) 49 | 50 | if not object then 51 | return false 52 | end 53 | 54 | local refId = object.refId 55 | local count = object.count or 1 56 | local charge = object.charge or -1 57 | local posX, posY, posZ = object.location.posX, object.location.posY, object.location.posZ 58 | local rotX, rotY, rotZ = object.location.rotX, object.location.rotY, object.location.rotZ 59 | local refIndex = refIndex 60 | 61 | local splitIndex = refIndex:split("-") 62 | 63 | --First, delete the original 64 | tes3mp.InitializeEvent(pid) 65 | tes3mp.SetEventCell(cell) 66 | tes3mp.SetObjectRefNumIndex(0) 67 | tes3mp.SetObjectMpNum(splitIndex[2]) 68 | tes3mp.AddWorldObject() --? 69 | tes3mp.SendObjectDelete() 70 | 71 | --LoadedCells[cell]:SaveObjectsDeleted(pid) 72 | 73 | --[[ 74 | if LoadedCells[cell] ~= nil then 75 | LoadedCells[cell].data.objectData[refIndex] = nil 76 | tableHelper.removeValue(LoadedCells[cell].data.packets.place, refIndex) 77 | tableHelper.removeValue(LoadedCells[cell].data.packets.delete, refIndex) 78 | end 79 | ]] 80 | 81 | --Now remake it 82 | tes3mp.InitializeEvent(pid) 83 | tes3mp.SetEventCell(cell) 84 | tes3mp.SetObjectRefId(refId) 85 | tes3mp.SetObjectCount(count) 86 | tes3mp.SetObjectCharge(charge) 87 | tes3mp.SetObjectPosition(posX, posY, posZ) 88 | tes3mp.SetObjectRotation(rotX, rotY, rotZ) 89 | tes3mp.SetObjectRefNumIndex(0) 90 | tes3mp.SetObjectMpNum(splitIndex[2]) 91 | tes3mp.AddWorldObject() 92 | tes3mp.SendObjectPlace() 93 | 94 | LoadedCells[cell]:Save() --Not needed, but it's nice to do anyways 95 | 96 | --LoadedCells[cell]:SaveObjectsPlaced(pid) 97 | 98 | --It works? IT WORKS!? Through a random combination of commenting out random things that as far as I can tell are vitally important, I have managed to get this bit working! 99 | end 100 | 101 | 102 | local function showPromptGUI(pid) 103 | local message = "[" .. playerCurrentMode[tes3mp.GetName(pid)] .. "] - Enter a number to add/subtract." 104 | 105 | tes3mp.InputDialog(pid, config.PromptId, message) 106 | end 107 | 108 | local function onEnterPrompt(pid, data) 109 | local cell = tes3mp.GetCell(pid) 110 | local pname = tes3mp.GetName(pid) 111 | local mode = playerCurrentMode[pname] 112 | local data = tonumber(data) or 0 113 | 114 | local object = getObject(playerLastObject[pname], cell) 115 | 116 | if not object then 117 | --The object no longer exists, so we should bail out now 118 | return false 119 | end 120 | 121 | if mode == "Rotate X" then 122 | local curDegrees = math.deg(object.location.rotX) 123 | local newDegrees = (curDegrees + data) % 360 124 | object.location.rotX = math.rad(newDegrees) 125 | elseif mode == "Rotate Y" then 126 | local curDegrees = math.deg(object.location.rotY) 127 | local newDegrees = (curDegrees + data) % 360 128 | object.location.rotY = math.rad(newDegrees) 129 | elseif mode == "Rotate Z" then 130 | local curDegrees = math.deg(object.location.rotZ) 131 | local newDegrees = (curDegrees + data) % 360 132 | object.location.rotZ = math.rad(newDegrees) 133 | elseif mode == "Move North" then 134 | object.location.posY = object.location.posY + data 135 | elseif mode == "Move East" then 136 | object.location.posX = object.location.posX + data 137 | elseif mode == "Move Up" then 138 | object.location.posZ = object.location.posZ + data 139 | elseif mode == "Scale Up" then 140 | --Not sure how scale works atm. 141 | --TODO 142 | return 143 | end 144 | 145 | for pid, pdata in pairs(Players) do 146 | resendPlace(pid, playerLastObject[pname], cell) 147 | end 148 | end 149 | 150 | local function showMainGUI(pid) 151 | --Determine if the player has an item 152 | local currentItem = "None" --default 153 | if playerLastObject[tes3mp.GetName(pid)] and getObject(playerLastObject[tes3mp.GetName(pid)], tes3mp.GetCell(pid)) then --If they have an entry and it isn't gone 154 | currentItem = playerLastObject[tes3mp.GetName(pid)] 155 | end 156 | 157 | local message = "Select an option. Your current item: " .. currentItem 158 | tes3mp.CustomMessageBox(pid, config.MainId, message, "Move North;Move East;Move Up;Rotate X;Rotate Y;Rotate Z;Scale Up;Grab;Close") 159 | end 160 | 161 | Methods.OnObjectPlace = function(pid, cellDescription) 162 | --Get the last event, which should hopefully be the place packet 163 | tes3mp.ReadLastEvent() 164 | 165 | --Get the refIndex of the first item in the object place packet (in theory, there should only by one) 166 | local refIndex = tes3mp.GetObjectRefNumIndex(0) .. "-" .. tes3mp.GetObjectMpNum(0) 167 | 168 | --Record that item as the last one the player interacted with in this cell 169 | playerLastObject[tes3mp.GetName(pid)] = refIndex 170 | end 171 | 172 | Methods.OnGUIAction = function(pid, idGui, data) 173 | local pname = tes3mp.GetName(pid) 174 | 175 | if idGui == config.MainId then 176 | if tonumber(data) == 0 then --Move North 177 | playerCurrentMode[pname] = "Move North" 178 | showPromptGUI(pid) 179 | return true 180 | elseif tonumber(data) == 1 then --Move East 181 | playerCurrentMode[pname] = "Move East" 182 | showPromptGUI(pid) 183 | return true 184 | elseif tonumber(data) == 2 then --Move Up 185 | playerCurrentMode[pname] = "Move Up" 186 | showPromptGUI(pid) 187 | return true 188 | elseif tonumber(data) == 3 then --Rotate X 189 | playerCurrentMode[pname] = "Rotate X" 190 | showPromptGUI(pid) 191 | return true 192 | elseif tonumber(data) == 4 then --Rotate Y 193 | playerCurrentMode[pname] = "Rotate Y" 194 | showPromptGUI(pid) 195 | return true 196 | elseif tonumber(data) == 5 then --Rotate Z 197 | playerCurrentMode[pname] = "Rotate Z" 198 | showPromptGUI(pid) 199 | return true 200 | elseif tonumber(data) == 6 then --Scale Up 201 | --TODO 202 | tes3mp.MessageBox(pid, -1, "Not yet implemented, sorry.") 203 | return true 204 | elseif tonumber(data) == 7 then --Grab 205 | --TODO 206 | tes3mp.MessageBox(pid, -1, "Not yet implemented, sorry.") 207 | return true 208 | elseif tonumber(data) == 8 then --Close 209 | --Do nothing 210 | return true 211 | end 212 | elseif idGui == config.PromptId then 213 | if data ~= nil and data ~= "" and tonumber(data) then 214 | onEnterPrompt(pid, data) 215 | end 216 | 217 | playerCurrentMode[tes3mp.GetName(pid)] = nil 218 | return true, showMainGUI(pid) 219 | end 220 | end 221 | 222 | Methods.OnPlayerCellChange = function(pid) 223 | playerLastObject[tes3mp.GetName(pid)] = nil 224 | end 225 | 226 | Methods.OnCommand = function(pid) 227 | showMainGUI(pid) 228 | end 229 | 230 | return Methods 231 | -------------------------------------------------------------------------------- /flatModifiers.lua: -------------------------------------------------------------------------------- 1 | -- flatModifiers (Advanced) - Release 1.1 - For tes3mp v0.6.1. Requires classInfo. 2 | 3 | --[[ INSTALLATION 4 | 1) Save this file as "flatModifiers.lua" in mp-stuff/scripts 5 | 2) Add [ flatModifiers = require("flatModifiers") ] to the top of server.lua 6 | 3) Add the following to OnPlayerLevel in server.lua 7 | [ flatModifiers.OnPlayerLevel(pid) ] 8 | 4) Add the following to OnPlayerSkill in server.lua 9 | [ flatModifiers.OnPlayerSkill(pid) ] 10 | ]] 11 | 12 | --[[ NOTE 13 | The values this script use are for fake level ups towards attributes, rather than the set multiplier to provide the attribute. These level ups translate as the following: 14 | 1-4 gives 2x, 5-7 gives 3x, 8-9 gives 4x, 10+ gives 5x 15 | ]] 16 | 17 | local Methods = {} 18 | 19 | classInfo = require("classInfo") 20 | 21 | local config = {} 22 | 23 | config.mode = "basic" -- "basic" or "class" 24 | --Basic mode sets all attribute increases to a flat value (determined by config.basicAttributeIncreases) 25 | --Class mode has attribute increases tailored to the character's class (see "class mode config options" section for config options) 26 | 27 | --globally used config options: 28 | config.includeLuck = false --Whether to include Luck in the calculations. By default, Morrowind doesn't allow bonuses to Luck. 29 | 30 | --basic mode config options: 31 | config.basicAttributeIncreases = 6 -- Number of skill increases towards the stats that the script should fake. 32 | 33 | --class mode config options: 34 | config.classBase = 3 -- How many skill advances every attribute has for its base 35 | config.classMajorSkillBonus = 1.5 -- How many skill advances get added to an attribute per major skill governed by it 36 | config.classMinorSkillBonus = 1 -- How many skill advances get added to an attribute per minor skill governed by it 37 | config.classAttributeBonus = 3 -- How many skill advances get added to an attribute which is one of the class' major attributes 38 | 39 | local function basicMode(pid) 40 | for i = 0, 7 do 41 | if i ~= 7 or config.includeLuck then --Avoid giving Luck (7) any bonus, unless configured to 42 | tes3mp.SetSkillIncrease(pid, i, config.basicAttributeIncreases) 43 | end 44 | end 45 | tes3mp.SendSkills(pid) 46 | end 47 | 48 | local function classMode(pid) 49 | local pClass = classInfo.GetPlayerClassData(pid) 50 | 51 | local changes = {} 52 | 53 | --Major attributes 54 | for k, v in pairs(pClass.majorAttributes) do 55 | changes[v] = (changes[v] or 0) + config.classAttributeBonus 56 | end 57 | 58 | --Major skills 59 | for k, v in pairs(pClass.majorSkills) do 60 | local governed = classInfo.GetGovernedAttribute(v) 61 | changes[governed] = (changes[governed] or 0) + config.classMajorSkillBonus 62 | end 63 | 64 | --Minor skills 65 | for k, v in pairs(pClass.minorSkills) do 66 | local governed = classInfo.GetGovernedAttribute(v) 67 | changes[governed] = (changes[governed] or 0) + config.classMinorSkillBonus 68 | end 69 | 70 | --Finally make the changes 71 | for i = 0, 7 do 72 | if i ~= 7 or config.includeLuck then --Avoid giving Luck (7) any bonus, unless configured to 73 | local amount = config.classBase + (changes[i] or 0) 74 | amount = math.floor(amount) 75 | tes3mp.SetSkillIncrease(pid, i, amount) 76 | end 77 | end 78 | 79 | tes3mp.SendSkills(pid) 80 | end 81 | 82 | local function doTheThing(pid) 83 | if config.mode == "basic" then 84 | basicMode(pid) 85 | else 86 | classMode(pid) 87 | end 88 | end 89 | 90 | 91 | Methods.OnPlayerLevel = function(pid) 92 | doTheThing(pid) 93 | end 94 | 95 | Methods.OnPlayerSkill = function(pid) 96 | doTheThing(pid) 97 | end 98 | 99 | return Methods 100 | -------------------------------------------------------------------------------- /flatModifiersBasic.lua: -------------------------------------------------------------------------------- 1 | -- flatModifiersBasic - Release 1 - For tes3mp v0.6.1 2 | 3 | --[[ INSTALLATION 4 | 1) Save this file as "flatModifiersBasic.lua" in mp-stuff/scripts 5 | 2) Add [ flatModifiersBasic = require("flatModifiersBasic") ] to the top of server.lua 6 | 3) Add the following to OnPlayerLevel in server.lua 7 | [ flatModifiersBasic.OnPlayerLevel(pid) ] 8 | 4) Add the following to OnPlayerSkill in server.lua 9 | [ flatModifiersBasic.OnPlayerSkill(pid) ] 10 | ]] 11 | 12 | Methods = {} 13 | 14 | local config = {} 15 | config.attributeIncreases = 6 -- Number of skill increases towards the stats that the script should fake. 1-4 gives 2x, 5-7 gives 3x, 8-9 gives 4x, 10+ gives 5x 16 | config.includeLuck = false --Whether to include Luck in the calculations. By default, Morrowind doesn't allow bonuses to Luck. 17 | 18 | local function setIncreases(pid) 19 | local pIncreases = Players[pid].data.attributeSkillIncreases 20 | for k, v in pairs(pIncreases) do 21 | if k ~= "Luck" or config.includeLuck then 22 | tes3mp.SetSkillIncrease(pid, tes3mp.GetAttributeId(k), config.attributeIncreases) 23 | end 24 | end 25 | tes3mp.SendSkills(pid) 26 | end 27 | 28 | Methods.OnPlayerLevel = function(pid) 29 | setIncreases(pid) 30 | end 31 | 32 | Methods.OnPlayerSkill = function(pid) --Conveniently, this also gets called during the player setup when they join the server 33 | setIncreases(pid) 34 | end 35 | 36 | return Methods 37 | -------------------------------------------------------------------------------- /kanaHousing/README.md: -------------------------------------------------------------------------------- 1 | # kanaHousing 2 | My own take on player housing, inspired by mupf's realEstate. 3 | ## Features 4 | * Players can buy and manage their own houses through a GUI. 5 | * Locking - Players can lock their houses, preventing others from entering. The owner, co-owners, and admins may enter the locked house whereas others will be turned away. Includes the ability to designate cells as important for travelling through (if entering it is required for a quest, for example), which allows regular members to enter while the cell is locked, so you don't have to worry so much about what places you designate as houses. 6 | * Owners can warp to any of their owned houses, though this feature can be disabled in the configs. 7 | * House owners can add co-owners, who can place or remove items in the house as well as pass through when the house is locked. 8 | * Admin GUI for creating, defining, and managing houses while in game. 9 | * While not strictly enforced, the script will report any players taking items from other player's houses in the server log, though it's not too difficult to expand what's already there if you want to do something with those dirty thieves. The script also differentiates between players taking regular items, and them taking items marked in the cell's data as quest items (the latter is okay). 10 | * Support for cell resets - Any doors associated with a house will automatically unlock if they should ever be locked, players can be allowed to pass through cells associated with quests, and the foundation has been laid for a server script to use to allow targeted resetting 11 | * Support for kanaFurniture. Owners and Co-owners are automatically given permission to place furniture in their houses. 12 | ## Usage 13 | ### Commands 14 | * `/houseinfo` - Use while in a house to view information on the house, as well as purchase it, if available. 15 | * `/house` - Used by players to view a list of all available houses on the server, as well as manage the settings of the houses that they own. 16 | * `/adminhouse` - Used by admins to edit and create new houses. 17 | 18 | ### Editing the files 19 | Some features, such as the reset info and a house's doors require manually editing the script's data file (found in data/kanaHousing.json). The structures of the script's data is outlined in the comments of the script's `createNewHouse` and `createNewCell`, if you wish to know how it all works. The following is an example of the script's data configured to include the single house: Chun-Ook, the boat in Ebonheart. It was chosen because it features two locked exterior doors, one "regular" cell (the cabin); one cell that requires passing through for a quest as well as an interior locked door (the upper deck); and one cell that requires passing through, contains quest items that'd need resetting (if you wish for multiple players to do the quest), and contains owned containers (the lower deck). 20 | ``` 21 | { 22 | "cells":{ 23 | "Chun-Ook, Upper Level":{ 24 | "house":"Ebonheart, Chun-Ook", 25 | "ownedContainers":false, 26 | "name":"Chun-Ook, Upper Level", 27 | "requiredAccess":true, 28 | "resetInfo":[], 29 | "requiresResets":false 30 | }, 31 | "Chun-Ook, Lower Level":{ 32 | "house":"Ebonheart, Chun-Ook", 33 | "ownedContainers":true, 34 | "name":"Chun-Ook, Lower Level", 35 | "requiredAccess":true, 36 | "resetInfo":[{ 37 | "instruction":"refill", 38 | "refId":"crate_01_limeware_uniqu", 39 | "refIndex":"297593-0" 40 | }], 41 | "requiresResets":true 42 | }, 43 | "Chun-Ook, Cabin":{ 44 | "house":"Ebonheart, Chun-Ook", 45 | "ownedContainers":false, 46 | "name":"Chun-Ook, Cabin", 47 | "requiredAccess":false, 48 | "resetInfo":[], 49 | "requiresResets":false 50 | } 51 | }, 52 | "owners":[], 53 | "houses":{ 54 | "Ebonheart, Chun-Ook":{ 55 | "name":"Ebonheart, Chun-Ook", 56 | "cells":{ 57 | "Chun-Ook, Upper Level":true, 58 | "Chun-Ook, Lower Level":true, 59 | "Chun-Ook, Cabin":true 60 | }, 61 | "price":5000, 62 | "outside":{ 63 | "pos":{ 64 | "y":-102871.0859375, 65 | "x":20961.751953125, 66 | "z":106.32440185547 67 | }, 68 | "cell":"2, -13" 69 | }, 70 | "doors":{ 71 | "2, -13":[{ 72 | "refIndex":"294940-0", 73 | "refId":"ex_de_ship_cabindoor" 74 | },{ 75 | "refIndex":"297456-0", 76 | "refId":"ex_de_ship_trapdoor" 77 | }], 78 | "Chun-Ook, Upper Level":[{ 79 | "refIndex":"297541-0", 80 | "refId":"in_de_shipdoor_toplevel" 81 | }] 82 | }, 83 | "inside":{ 84 | "pos":{ 85 | "y":-269.77154541016, 86 | "x":-113.86807250977, 87 | "z":-172.68469238281 88 | }, 89 | "cell":"Chun-Ook, Cabin" 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | 97 | ### Scripts 98 | There are a number of functions exposed for other scripts to use, which can be found towards the bottom of the script (more can be easily added) - I will probably try to write up what they are and what they do here eventually. 99 | 100 | ## Known Issues 101 | I don't know of any issues and have tried to test everything to make sure it works, but it's possible that something slipped through the net and made it into the release. If you run into any problems, feel free to contact me so I can get things fixed! You're most likely to find me lurking in the [tes3mp Discord channel](https://discord.gg/ECJk293). 102 | -------------------------------------------------------------------------------- /markRecall.lua: -------------------------------------------------------------------------------- 1 | local Methods = {} 2 | 3 | --[[ INSTALLATION: 4 | 1) Save this file as "markRecall.lua" in mp-stuff/scripts 5 | 2) Add [ markRecall = require("markRecall") ] to the top of server.lua 6 | 3) Add the following to the elseif chain for commands in "OnPlayerSendMessage" inside server.lua 7 | 8 | [ elseif (cmd[1] == "mark") then 9 | markRecall.OnMarkCommand(pid) 10 | elseif (cmd[1] == "recall") then 11 | markRecall.OnRecallCommand(pid) ] 12 | 13 | ]] 14 | 15 | --[[ USAGE: 16 | Type "/mark" into chat to save your current position 17 | Type "/recall" into chat to return to that position 18 | 19 | See the config section for how you can configure these functions 20 | 21 | You can use [ markRecall.SetCanRecall(pid, [0/1]) ] in your own scripts to disable/enable a player's ability to recall, and [ markRecall.isRecallEnabled(pid) ] for a boolean result that reports if the player has or hasn't had their recall abilities disabled in this way. 22 | ]] 23 | 24 | --[[ DEVELOPMENT: 25 | Version 3 26 | For TES3MP v0.6.1 27 | =TODO= 28 | - Additional option to allow players with any means (Amulet of Recall, Potion of Marking, etc.) to use the commands 29 | - And then also have potions consumed/charges used if using the command 30 | - Option to pay money to recall? 31 | =Changelog= 32 | Version 3: 33 | - For some reason, if isRecallEnabled was Methods.isRecallEnabled, the server would crash whenever it was called... even though it's almost exactly the same in serverWarp, which works fine. For now I've change isRecallEnabled to be a regular function, which means other scripts can't access it anymore. This change might be temporary, or it might be permanent (seeing as I can't find ANY reason why it shouldn't work... :<) 34 | ]] 35 | 36 | --CONFIG 37 | local config = {} 38 | 39 | --If requireSpells is true, the player is required to have the spells in their spellbook if they want to be able to use the respective commands 40 | config.requireSpells = false 41 | 42 | --Any cell listed in blacklistCells won't allow for a player to place a Mark in them. See the commented out line below for an example of how to add a cell to the blacklist. 43 | config.blacklistCells = {} 44 | -- config.blacklistCells["Balmora, Hlaalo Manor"] = true 45 | 46 | --/CONFIG 47 | 48 | Methods.OnMarkCommand = function(pid) 49 | --Check the player has the mark spell if it's required 50 | if config.requireSpells and (hasSpell(pid, "mark") == false) then 51 | tes3mp.SendMessage(pid, "You don't know the Mark spell.\n", false) 52 | return false 53 | --Check player is in a non-blacklisted cell 54 | elseif config.blacklistCells[tes3mp.GetCell(pid)] then 55 | tes3mp.SendMessage(pid, "You're not allowed to set a Mark in this location.\n", false) 56 | return false 57 | end 58 | 59 | Players[pid].data.customVariables.markSpot = {} 60 | 61 | local ms = Players[pid].data.customVariables.markSpot 62 | 63 | ms.posX = tes3mp.GetPosX(pid) 64 | ms.posY = tes3mp.GetPosY(pid) 65 | ms.posZ = tes3mp.GetPosZ(pid) 66 | ms.cell = tes3mp.GetCell(pid) 67 | ms.rotX = tes3mp.GetRotX(pid) 68 | ms.rotZ = tes3mp.GetRotZ(pid) 69 | 70 | Players[pid]:Save() 71 | 72 | tes3mp.SendMessage(pid, "Mark location set.\n", false) 73 | end 74 | 75 | Methods.OnRecallCommand = function(pid) 76 | --Check the player has a Mark spot 77 | if Players[pid].data.customVariables.markSpot == nil then 78 | tes3mp.SendMessage(pid, "You don't have a Marked spot. Use /mark to set one.\n", false) 79 | return false 80 | --Check the player has the recall spell if it's required 81 | elseif config.requireSpells and (hasSpell(pid, "recall") == false) then 82 | tes3mp.SendMessage(pid, "You don't know the Recall spell.\n", false) 83 | return false 84 | --Check if the player's ability to recall isn't disabled by another script 85 | elseif isRecallEnabled(pid) == false then 86 | tes3mp.SendMessage(pid, "You can't Recall at this time.\n", false) 87 | return false 88 | end 89 | 90 | local ms = Players[pid].data.customVariables.markSpot 91 | --Following basically copied from myMod.lua Teleport script 92 | tes3mp.SetCell(pid, ms.cell) 93 | tes3mp.SendCell(pid) 94 | 95 | tes3mp.SetPos(pid, ms.posX, ms.posY, ms.posZ) 96 | tes3mp.SetRot(pid, ms.rotX, ms.rotZ) 97 | tes3mp.SendPos(pid) 98 | 99 | tes3mp.SendMessage(pid, "You have Recalled.\n", false) 100 | end 101 | 102 | Methods.SetCanRecall = function(pid, val) 103 | --Use 0 to disable, 1 to enable 104 | Players[pid].data.customVariables.canRecall = val 105 | Players[pid]:Save() 106 | end 107 | 108 | function isRecallEnabled(pid) 109 | if tonumber(Players[pid].data.customVariables.canRecall) == 0 then 110 | return false 111 | else 112 | return true 113 | end 114 | end 115 | 116 | function hasSpell(pid, spell) 117 | local has = false 118 | for k,v in pairs(Players[pid].data.spellbook) do 119 | if v.spellId == spell then 120 | has = true 121 | break 122 | end 123 | end 124 | return has 125 | end 126 | 127 | return Methods 128 | -------------------------------------------------------------------------------- /serverWarp.lua: -------------------------------------------------------------------------------- 1 | local Methods = {} 2 | 3 | --[[ INSTALLATION: 4 | 1) Save this file as "serverWarp.lua" in mp-stuff/scripts 5 | 2) Add [ serverWarp = require("serverWarp") ] to the top of server.lua 6 | 3) Add the following to the elseif chain for commands in "OnPlayerSendMessage" inside server.lua 7 | 8 | [ elseif cmd[1] == "warp" and cmd[2] ~=nil then 9 | serverWarp.OnWarpCommand(pid, tableHelper.concatenateFromIndex(cmd, 2)) 10 | elseif (cmd[1] == "setwarp" or cmd[1] == "setpublicwarp") and cmd[2] ~= nil then 11 | serverWarp.OnSetWarpCommand(pid, tableHelper.concatenateFromIndex(cmd, 2), (cmd[1] == "setpublicwarp")) 12 | elseif (cmd[1] == "removewarp" or cmd[1] == "removepublicwarp") and cmd[2] ~= nil then 13 | serverWarp.OnRemoveWarpCommand(pid, tableHelper.concatenateFromIndex(cmd, 2), (cmd[1] == "removepublicwarp")) 14 | elseif cmd[1] == "warplist" then 15 | serverWarp.OnWarpListCommand(pid) 16 | elseif cmd[1] == "forcewarp" and cmd[2] ~= nil and cmd[3] ~= nil then 17 | serverWarp.OnForcePlayerCommand(pid, cmd[2], tableHelper.concatenateFromIndex(cmd, 3)) 18 | elseif cmd[1] == "jailwarp" and cmd[2] ~= nil and cmd[3] ~= nil then 19 | serverWarp.OnJailPlayerCommand(pid, cmd[2], tableHelper.concatenateFromIndex(cmd, 3)) 20 | elseif cmd[1] == "allowwarp" and cmd[2] ~= nil and cmd[3] ~= nil then 21 | serverWarp.OnSetCanWarpCommand(pid, cmd[2], cmd[3]) ] 22 | 23 | ]] 24 | 25 | --[[ USAGE: 26 | Command list: 27 | /warplist 28 | Prints a list of all public warps and your own private warps into chat 29 | /warp [ warp name ] 30 | Requires permission: useWarpRank 31 | Warp yourself to a warp with the provided name. It first checks your personal warp list and if it can't find a warp by that name it then checks the public warp list. You can't use this command if your warp privilege has been disabled. 32 | /setwarp [ warp name ] 33 | Requires permission: setWarpRank 34 | Records your current position as a personal warp point with the provided name 35 | /setpublicwarp [ warp name ] 36 | Requires permission: setPublicWarpRank 37 | Records your current position as a public warp point with the provided name 38 | /removewarp [ warp name ] 39 | Requires permission: setWarpRank 40 | Removes the named warp from your personal warp list 41 | /removepublicwarp [ warp name ] 42 | Requires permission: removePublicWarpRank 43 | Removes the named warp from the public warp list 44 | /forcewarp [ target player's id ] [ warp name (of a public warp) ] 45 | Requires permission: forcePlayerRank 46 | Forcibly teleports the player with the provided id to a public warp with the given name 47 | /jailwarp [ target player's id ] [ warp name (of a public warp) ] 48 | Requires permission: forcePlayerRank AND forceJailPlayerRank 49 | As with /forcewarp, but also disables the player's warp privileges 50 | /allowwarp [ player id ] [ 0/1 to disable/enable ] 51 | Requires permission: setAllowWarp 52 | Sets the targeted player's warp privileges. Set to 0 to disable them from using the /warp command, set to 1 to enable them again. 53 | Example usage: 54 | /forcewarp 1 the forum 55 | 56 | Every single method in this script can be used via custom scripts. Knock yourself out. 57 | ]] 58 | 59 | --[[ DEVELOPMENT: 60 | Version 1 61 | For TES3MP v0.6.1 62 | =TODO= 63 | - More features? 64 | =Notes= 65 | Warpdata structure: 66 | cell 67 | posX 68 | posY 69 | posZ 70 | rotX 71 | rotZ 72 | ]] 73 | 74 | 75 | local config = {} 76 | --The minimum rank required to perform any of the actions. 0 is a regular player, 1 is a moderator and 2 is an admin. 77 | config.setWarpRank = 0 --Also used to determine if they can remove their own warps 78 | config.setPublicWarpRank = 1 79 | config.useWarpRank = 0 --Don't differentiate between Public and Private warps for simplicity 80 | config.removePublicWarpRank = 1 81 | config.forcePlayerRank = 1 82 | config.forceJailPlayerRank = 1 --Players also require the permissions to forcePlayer to use this command. 83 | config.setAllowWarp = 1 84 | 85 | 86 | Methods.OnSetWarpCommand = function(pid, warpName, isPublic) 87 | local rank = Players[pid].data.settings.admin 88 | 89 | --Check player has the correct rank 90 | if isPublic and (rank < config.setPublicWarpRank) then 91 | tes3mp.SendMessage(pid, "Your rank is too low to set Public Warps.\n", false) 92 | return false 93 | elseif rank < config.setWarpRank then 94 | tes3mp.SendMessage(pid, "Your rank is too low to set Warps.\n", false) 95 | return false 96 | end 97 | 98 | local newWarp = {} 99 | 100 | newWarp.cell = tes3mp.GetCell(pid) 101 | newWarp.posX = tes3mp.GetPosX(pid) 102 | newWarp.posY = tes3mp.GetPosY(pid) 103 | newWarp.posZ = tes3mp.GetPosZ(pid) 104 | newWarp.rotX = tes3mp.GetRotX(pid) 105 | newWarp.rotZ = tes3mp.GetRotZ(pid) 106 | 107 | --Note: Will overwrite existing warps of the same name 108 | if isPublic then 109 | Methods.AddPublicWarp(warpName, newWarp) 110 | else 111 | Methods.AddPrivateWarp(pid, warpName, newWarp) 112 | end 113 | 114 | tes3mp.SendMessage(pid, "Warp added.\n", false) 115 | return true 116 | end 117 | 118 | Methods.OnRemoveWarpCommand = function(pid, warpName, isPublic) 119 | local rank = Players[pid].data.settings.admin 120 | --Check player permissions 121 | if isPublic and (rank < config.removePublicWarpRank) then 122 | tes3mp.SendMessage(pid, "Your rank is too low to remove Public Warps.\n", false) 123 | return false 124 | --Doesn't have a unique config entry - if the player can set private warps, they're allowed to delete them 125 | elseif rank < config.setWarpRank then 126 | tes3mp.SendMessage(pid, "Your rank is too low to remove Warps.\n", false) 127 | return false 128 | end 129 | 130 | --Find the Warp to remove 131 | local list 132 | 133 | if isPublic then 134 | list = Methods.GetPublicWarps() 135 | else 136 | list = Methods.GetPrivateWarps(pid) 137 | end 138 | 139 | --If the Warp exists, remove it, otherwise error 140 | --(This should probably be in its own function...) 141 | local warpName = string.lower(warpName) 142 | if list[warpName] ~= nil then 143 | list[warpName] = nil 144 | if isPublic then 145 | WorldInstance:Save() 146 | tes3mp.SendMessage(pid, "Removed Public Warp by the name '" .. warpName .. "'.\n", false) 147 | else 148 | Players[pid]:Save() 149 | tes3mp.SendMessage(pid, "Removed Warp by the name '" .. warpName .. "'.\n", false) 150 | end 151 | return true 152 | else 153 | tes3mp.SendMessage(pid, "Couldn't find a Warp by the name '" .. warpName .. "'.\n", false) 154 | return false 155 | end 156 | end 157 | 158 | Methods.OnWarpCommand = function(pid, warpName) 159 | local rank = Players[pid].data.settings.admin 160 | 161 | --Check the player can warp 162 | if Methods.isWarpEnabled(pid) == false then 163 | tes3mp.SendMessage(pid, "You can't Warp at this time.\n", false) 164 | return false 165 | --Check their rank 166 | elseif rank < config.useWarpRank then 167 | tes3mp.SendMessage(pid, "Your rank is too low to use Warps.\n", false) 168 | return false 169 | end 170 | 171 | local foundWarp = Methods.FindWarp(warpName, pid, false) --Prioritises private warps over public ones 172 | 173 | if foundWarp then 174 | Methods.WarpPlayer(pid, foundWarp) 175 | tes3mp.SendMessage(pid, "You have Warped to " .. warpName ..".\n", false) 176 | return true 177 | else 178 | tes3mp.SendMessage(pid, "Couldn't find a Warp with that name.\n", false) 179 | return false 180 | end 181 | end 182 | 183 | Methods.OnWarpListCommand = function(pid) 184 | local pubWarps = Methods.GetPublicWarps() 185 | local privWarps = Methods.GetPrivateWarps(pid) 186 | 187 | --Public warps list 188 | local message = "Public Warps:\n" 189 | for k, v in pairs(pubWarps) do 190 | message = message .. "> " .. k .. "\n" 191 | end 192 | --Private warps list 193 | message = message .. "Your Warps:\n" 194 | for k, v in pairs(privWarps) do 195 | message = message .. "> " .. k .. "\n" 196 | end 197 | 198 | tes3mp.SendMessage(pid, message, false) 199 | end 200 | 201 | Methods.OnForcePlayerCommand = function (pid, targetId, warpName, cantWarp) 202 | local rank = Players[pid].data.settings.admin 203 | 204 | if rank < config.forcePlayerRank then 205 | tes3mp.SendMessage(pid, "Your rank is too low to force Warp players.\n", false) 206 | return false 207 | end 208 | 209 | local foundWarp = Methods.FindWarp(warpName, nil, true) 210 | 211 | if foundWarp then 212 | Methods.WarpPlayer(targetId, foundWarp) 213 | 214 | --Only gets cantWarp when called through the OnJailPlayerCommand 215 | if cantWarp then 216 | Methods.SetCanWarp(targetId, 0) 217 | end 218 | 219 | tes3mp.SendMessage(pid, "Warped " .. --[[Players[targetId].name]] "player" .. " to " .. warpName .. ".\n", false) 220 | tes3mp.SendMessage(targetId, "You were warped to " .. warpName .. " by " .. Players[pid].name .. ".\n", false) 221 | 222 | return true 223 | else 224 | tes3mp.SendMessage(pid, "Couldn't find the Warp.\n", false) 225 | return false 226 | end 227 | 228 | 229 | end 230 | 231 | --Basically uses existing commands to teleport a player to a specific warp and disable their ability to warp. 232 | Methods.OnJailPlayerCommand = function(pid, targetId, warpName) 233 | local rank = Players[pid].data.settings.admin 234 | 235 | if rank < config.forceJailPlayerRank then 236 | tes3mp.SendMessage(pid, "Your rank is too low to jail players.\n", false) 237 | return false 238 | end 239 | 240 | return Methods.OnForcePlayerCommand(pid, targetId, warpName, true) 241 | end 242 | 243 | Methods.OnSetCanWarpCommand = function(pid, targetId, value) 244 | --DEBUG 245 | tes3mp.SendMessage(pid, "targetId: " .. targetId .. " value: " .. value .. "\n", false) 246 | local rank = Players[pid].data.settings.admin 247 | 248 | if rank < config.setAllowWarp then 249 | tes3mp.SendMessage(pid, "Your rank is too low to change a player's warp privileges.\n", false) 250 | return false 251 | end 252 | 253 | Methods.SetCanWarp(targetId, value) 254 | end 255 | 256 | Methods.SetCanWarp = function(pid, val) 257 | --Make sure the arguments are valid 258 | local pid = tonumber(pid) 259 | local val = tonumber(val) 260 | --Use 0 to disable, 1 to enable 261 | Players[pid].data.customVariables.canServerWarp = val 262 | Players[pid]:Save() 263 | end 264 | 265 | Methods.isWarpEnabled = function(pid) 266 | if tonumber(Players[pid].data.customVariables.canServerWarp) == 0 then 267 | return false 268 | else 269 | return true 270 | end 271 | end 272 | 273 | Methods.GetPublicWarps = function() 274 | --If there are no public warps, create the table 275 | if WorldInstance.data.customVariables.serverWarp == nil then 276 | WorldInstance.data.customVariables.serverWarp = {} 277 | WorldInstance:Save() 278 | end 279 | 280 | return WorldInstance.data.customVariables.serverWarp 281 | end 282 | 283 | Methods.GetPrivateWarps = function(pid) 284 | --If there are no private warps, create the table 285 | if Players[pid].data.customVariables.serverWarp == nil then 286 | Players[pid].data.customVariables.serverWarp = {} 287 | Players[pid]:Save() 288 | end 289 | 290 | return Players[pid].data.customVariables.serverWarp 291 | end 292 | 293 | Methods.AddPublicWarp = function(warpName, data) 294 | local warps = Methods.GetPublicWarps() 295 | local warpName = string.lower(warpName) 296 | 297 | warps[warpName] = data 298 | WorldInstance:Save() 299 | end 300 | 301 | Methods.AddPrivateWarp = function(pid, warpName, data) 302 | local warps = Methods.GetPrivateWarps(pid) 303 | local warpName = string.lower(warpName) 304 | 305 | warps[warpName] = data 306 | Players[pid]:Save() 307 | end 308 | 309 | Methods.FindWarp = function(warpName, pid, prioritisePublic) 310 | local pubWarps = Methods.GetPublicWarps() 311 | local privWarps 312 | 313 | local warpName = string.lower(warpName) 314 | 315 | local pubCheck = Methods.SearchWarps(pubWarps, warpName) 316 | local privCheck 317 | 318 | if pid then 319 | privWarps = Methods.GetPrivateWarps(pid) 320 | privCheck = Methods.SearchWarps(privWarps, warpName) 321 | end 322 | 323 | if prioritisePublic then 324 | return pubCheck or privCheck or false 325 | else 326 | return privCheck or pubCheck or false 327 | end 328 | end 329 | 330 | Methods.WarpPlayer = function(pid, warpData) 331 | tes3mp.SetCell(pid, warpData.cell) 332 | tes3mp.SendCell(pid) 333 | 334 | tes3mp.SetPos(pid, warpData.posX, warpData.posY, warpData.posZ) 335 | tes3mp.SetRot(pid, warpData.rotX, warpData.rotZ) 336 | tes3mp.SendPos(pid) 337 | end 338 | 339 | Methods.SearchWarps = function (warps, warpName) 340 | warpName = string.lower(warpName) --should never get to here without first being turned into lowercase, but we'll do it here again just in case 341 | for k,v in pairs(warps) do 342 | if k == warpName then 343 | return v 344 | end 345 | end 346 | 347 | return false 348 | end 349 | 350 | 351 | return Methods 352 | -------------------------------------------------------------------------------- /tes3mp FAQ.md: -------------------------------------------------------------------------------- 1 | ***Foreword:** The following is a copy of the [tes3mp FAQ](https://steamcommunity.com/groups/mwmulti/discussions/1/353916184342480541/) (Last updated: 29 Nov, 2017) as written by [David C.](https://github.com/davidcernat). The only reason I've made this copy is so I can link to specific FAQ questions, since the Steam forums don't support anchors (though Steam guides do?). I'm in no way affiliated with the people who actually make tes3mp, blah, blah, blah.* 2 | 3 | # Frequently Asked Questions 4 | ### What is this group about? 5 | In theory, it's about everything related to playing Morrowind in multiplayer. In practice, it's about playing Morrowind in multiplayer through [TES3MP](https://github.com/TES3MP/openmw-tes3mp) which is based on [OpenMW](https://github.com/OpenMW/openmw) 6 | 7 | The admin team of our Steam group also happens to be the development team of TES3MP. 8 | 9 | ### Where can I donate to TES3MP? 10 | You can donate to me on my own recently started [Patreon page](https://www.patreon.com/davidcernat). 11 | 12 | You can donate to my fellow developer Stanislav on his [Patreon page](https://www.patreon.com/Koncord). 13 | 14 | ### How do I use TES3MP if I'm on 64-bit Windows? 15 | It's quite easy. Follow the guide [here](http://steamcommunity.com/groups/mwmulti/discussions/2/353915309331818721/). 16 | 17 | ### Is TES3MP a multiplayer mod? 18 | No. Multiplayer mods are modifications to closed source original games that almost always have severe limitations in what they can achieve. There have been many examples of them and most have never gotten very far at all. 19 | 20 | TES3MP is the multiplayer branch of an open source recreation of Morrowind's engine called OpenMW, done from the ground up and using none of Morrowind's original engine code. As a result, we can add any features we want to it, and we will be adding a lot of ambitious ones as time goes by that would never be possible or even imaginable in a multiplayer mod. 21 | 22 | ### When I open up *tes3mp-browser.exe*, why do I not see any servers show up? 23 | Either the master server is down, or you have entered the wrong address and port for the master server in your *tes3mp-client-server.cfg* 24 | 25 | The address of our default master server is *master.tes3mp.com* and its port is 25560 26 | 27 | ### How do I join a server manually? 28 | If you do not wish to use the server browser, or have problems with it, you can always do a direct IP connection by simply editing the following lines in *tes3mp-client-default.cfg*: 29 | ``` 30 | destinationAddress = localhost 31 | port = 25565 32 | password = 33 | ``` 34 | Replace "localhost" with the IP of the server, put in the correct port, the correct password (if there is one) and then run *tes3mp.exe* to connect. 35 | 36 | ### How do I play LAN? 37 | When doing a direct connection as described in the answer above, simply set your destinationAddress to the server's local IP instead of its external, public IP. 38 | 39 | You can also prevent the server from appearing in the server browser by disabling the connection to the master server in tes3mp-server-default.cfg. Turn this: 40 | ``` 41 | [MasterServer] 42 | address = master.tes3mp.com 43 | enabled = true 44 | ``` 45 | Into: 46 | ``` 47 | [MasterServer] 48 | address = master.tes3mp.com 49 | enabled = false 50 | ``` 51 | 52 | ### When I run TES3MP, why does it say I'm missing vcruntime140.dll and msvcp140.dll ? 53 | You are missing the Visual C++ Redistributable for Visual Studio 2015 and need to install it. Get it from [here.](https://www.microsoft.com/en-us/download/details.aspx?id=48145) 54 | 55 | ### When I try to connect to a server, why am I prevented and told that my load order is "Bloodmoon.esm, Morrowind.esm and Tribunal.esm"? 56 | *openmw-launcher.exe* sometimes loads your .esm files in the wrong order. Try going to its Data Files tab and dragging the files around so they're in the correct order. 57 | 58 | If that doesn't work for you, use a text editor to open up the OpenMW configuration file found here: 59 | ``` 60 | C:\Users\Username\Documents\My Games\OpenMW\openmw.cfg 61 | ``` 62 | Scroll down to its bottom and set the order manually like this: 63 | ``` 64 | content=Morrowind.esm 65 | content=Tribunal.esm 66 | content=Bloodmoon.esm 67 | ``` 68 | 69 | ### Certain Morrowind mods require you to register .bsa files. How do I do that for TES3MP? 70 | Use a text editor to open up the OpenMW configuration file found here: 71 | ``` 72 | C:\Users\Username\Documents\My Games\OpenMW\openmw.cfg 73 | ``` 74 | Near the top will be all your registered .bsa files, in the following pattern: 75 | ``` 76 | fallback-archive=Morrowind.bsa 77 | fallback-archive=Tribunal.bsa 78 | fallback-archive=Bloodmoon.bsa 79 | ``` 80 | Simply add the ones you want to those. 81 | 82 | ### When I start TES3MP, I get an error about a missing .bsa archive. How do I fix it? 83 | Go to your list of registered .bsa archives as instructed in the answer above, then remove the line of the missing one. 84 | 85 | ### How do I host a server myself? 86 | Simply start up *tes3mp-server.exe*. Other players will be able to find your server using *tes3mp-browser.exe* as long as your server is set to communicate with the master server and as long as your server port (25565 by default) is forwarded correctly. 87 | 88 | ### Why does my server appear as having a ping of 999 in the server browser? 89 | The port set for your server in *tes3mp-server-default.cfg* (25565 by default) isn't forwarded correctly. You'll need to open up your router's configuration pages in your browser, go to the port forwarding section and forward the port to your local IP. When asked to choose a protocol from TCP, UDP and both, pick either UDP or both. 90 | 91 | This is a general computer problem, not something specifically related to TES3MP, and such you should find a guide – perhaps for your specific router – elsewhere on the internet, like on [www.portforward.com](https://portforward.com/) 92 | 93 | That being said, TES3MP's default port of 25565 is also used by Minecraft, and port forwarding guides written by the Minecraft community are just as applicable to TES3MP. 94 | 95 | After port forwarding, you may also want to ensure the TES3MP server is not blocked by your firewall. 96 | 97 | ### I've forwarded my port and other people can join my server through the server browser, but it still shows up as having a ping of 999 for me and my connect button is greyed out in the server browser. How can I join it? 98 | There seems to be a bug with the server browser where your own server will mistakenly have that ping. Nonetheless, you can always join yourself by running *tes3mp.exe* with "localhost" as your destinationAddress in your *tes3mp-client-default.cfg* 99 | 100 | ### How do I configure my server? 101 | To change your basic server options – such as the name of the server or the port used for it – simply edit your *tes3mp-server-default.cfg* file. 102 | 103 | For more advanced settings, read [this guide](http://steamcommunity.com/groups/mwmulti/discussions/1/133258593388999187/). 104 | 105 | ### Where do I find client and server log files? 106 | Open up this folder: 107 | ``` 108 | C:\Users\Username\Documents\My Games\OpenMW 109 | ``` 110 | Client log files start with tes3mp-client and server log files start with *tes3mp-server*. 111 | 112 | ### How are plugins handled? 113 | Servers have a file named pluginlist.json that lets them enforce a list of plugins in a certain order with specific checksums. That means players all need to enable the same plugins in openmw-launcher.exe as the server that they want to join. 114 | 115 | ### How do I set up the plugins for my server? 116 | Open up your *mp-stuff\data\pluginlist.json* and add the plugins you want, in the order you want them and with their corresponding accepted checksums. 117 | 118 | For instance, this will only accept the English GOTY editions of Morrowind, Tribunal and Bloodmoon: 119 | ``` 120 | "0": {"Morrowind.esm": ["0x7B6AF5B9"]}, 121 | "1": {"Tribunal.esm": ["0xF481F334"]}, 122 | "2": {"Bloodmoon.esm": ["0x43DD2132"]} 123 | ``` 124 | By default, pluginlist.json also accepts the Russian GOTY edition of Morrowind as a second set of checksums because of its compatibility with the English edition: 125 | ``` 126 | "0": {"Morrowind.esm": ["0x7B6AF5B9", "0x34282D67"]}, 127 | "1": {"Tribunal.esm": ["0xF481F334", "0x211329EF"]}, 128 | "2": {"Bloodmoon.esm": ["0x43DD2132", "0x9EB62F26"]} 129 | ``` 130 | If you like, you can also not put in any checksums at all: 131 | ``` 132 | "0": {"Morrowind.esm": []}, 133 | "1": {"Tribunal.esm": []}, 134 | "2": {"Bloodmoon.esm": []} 135 | ``` 136 | However, this will make it possible for anyone to join your server with pretty much any files named like that, which is why you should use checksums unless playing with trusted people. 137 | 138 | ### How do I get the checksums for the plugins I want to use on the server? 139 | The easiest way to do this that doesn't rely on any other software is to simply enable all the plugins you want in *openmw-launcher.exe*, connect to a server and get rejected from it. 140 | 141 | Afterwards, find your latest client log file here: 142 | ``` 143 | C:\Users\Username\Documents\My Games\OpenMW 144 | ``` 145 | Open it up and it will contain the checksums you need near the top: 146 | ``` 147 | idx: 0 checksum: 7B6AF5B9 file: C:\Games\Morrowind\Data Files\Morrowind.esm 148 | idx: 1 checksum: F481F334 file: C:\Games\Morrowind\Data Files\Tribunal.esm 149 | idx: 2 checksum: 43DD2132 file: C:\Games\Morrowind\Data Files\Bloodmoon.esm 150 | ``` 151 | Simply copy-paste the checksums into your pluginlist.json, add an "0x" in front of them and you're good to go. 152 | 153 | If, after doing that, your server crashes upon starting, you've probably put in a wrong character somewhere. Take a close look to make sure your pluginlist.json additions fit the pattern correctly. 154 | 155 | ### My server crashes whenever a certain player joins it. What is going on? 156 | It's possible that the data of the cell the player spawns in has somehow gotten corrupted. You should post about it on our forums or ask about it on our Discord so we can try to fix it for you and ensure it doesn't happen for others in the future. 157 | 158 | ### I don't have Morrowind installed in its English edition. Can I play with other people? 159 | If you have the French or German editions of Morrowind installed, you will not be able to join the vast majority of servers. 160 | 161 | The French and German editions are not compatible with other languages because they contain hardcoded translations of interior cell names and dialogue topics. You can still host a server with them as long as you put in their checksums in your mp-stuff\data\pluginlist.json, but you shouldn't try to combine them with other editions unless you want to experience severe issues. 162 | 163 | The Russian edition contains localization files providing a softcoded translation that is compatible with the English edition. 164 | 165 | We have not been able to try any other language editions. Feel free to provide us as much information as you can on them. 166 | 167 | ### I have the English edition of Morrowind, but don't have the same checksums for Morrowind, Tribunal or Bloodmoon as those used by servers. What is going on? 168 | Servers use the checksums of the English GOTY edition by default. 169 | 170 | If you are using the original CD version of Morrowind or one of its expansions, ensure that you have installed the latest official patches for them. 171 | 172 | If you are using the Steam version of Morrowind's GOTY edition, try verifying the integrity of your game files. 173 | 174 | ### How do I use TES3MP if I'm on Linux? 175 | We do not currently release binaries for Linux, because of how many different Linux distributions there are and because of how often our code changes. 176 | 177 | To use TES3MP on Linux, you'll have to build it yourself. Luckily, it's rather easy compared to building it on anything else. Simply use Grim Kriegor's [Linux build script](http://steamcommunity.com/groups/mwmulti/discussions/2/353915309331802029/). 178 | 179 | ### Why am I having problems joining a Linux server with my Windows client, or joining a Windows server with my Linux client? 180 | In order to be compatible, the client and server need to be built using the exact same version of the code, or they will refuse to connect to each other. 181 | 182 | Why do they have to refuse? Because there is a very good chance that otherwise some data packets will not match between the two, thus leading to freezes or crashes. 183 | 184 | If you do a Linux build using the same code that was used for the Windows release, they will work together fine. Simply look at when the last Windows release was posted and revert your local code repository to that date. 185 | 186 | ### How do I use TES3MP if I'm on 32-bit Windows or OSX? 187 | Alas, we do not currently have much help to offer for those situations, seeing as none of our developers have yet compiled TES3MP for either, and we haven't heard from anyone who has. 188 | 189 | ### How playable is TES3MP as of now? 190 | With my recent addition of NPC and quest sync, TES3MP has become reasonably playable as long as you don't log out during certain 191 | portions of questlines and accept certain limitations. 192 | 193 | The [announcement for the latest version](http://steamcommunity.com/groups/mwmulti#announcements/detail/1441567597386587897) summarizes all remaining problems. 194 | 195 | ### Does TES3MP receive the improvements that OpenMW does? 196 | TES3MP's code repository takes in all of OpenMW's changes on an almost daily basis, which means that every TES3MP release is based on the most recent OpenMW code as it existed at the time. 197 | 198 | ### Is TES3MP a part of OpenMW? 199 | TES3MP is currently regarded as more of a sister project to OpenMW, though we are open to the idea of merging with OpenMW in the future should their developers desire it. We also have a [subforum on the OpenMW website](https://forum.openmw.org/viewforum.php?f=44&sid=f17e20c7a99f9a5b16ca2c78734d511d) and we have been featured in their [news announcements](https://openmw.org/2017/openmw-multiplayer-here/). 200 | 201 | ### How large is the TES3MP team? 202 | After experimenting with OpenMW in the summer of 2015, Stanislav started adding multiplayer functionality to it in December of 2015. That was the birth of TES3MP, and he worked on it alone until the 8th of July 2016, when he first released its code on GitHub. 203 | 204 | That same day I created this group. After compiling TES3MP for Linux and writing a guide about it, I was invited by Stanislav to join TES3MP officially as a developer, and I've been fixing problems and adding features ever since. My main contributions so far are the massive features of NPC sync, quest sync, world sync and state saving/loading. 205 | 206 | ### How functional is the AI? 207 | AI works quite well now. There are a few situations where NPCs react differently to the client on whose their AI is running, by only greeting that particular player, by only following that player during quests, or by only trying to arrest that one player in the case of guards. However, the game is very playable even with those quirks, and they will be fixed for version 0.7.0 208 | 209 | ### How many people will be able to play on a server at the same time? 210 | There is no clear limit, but the server currently starts crashing after a few dozen due to the increased frequency of packets with invalid data. 211 | 212 | ### How do I enable distant terrain? 213 | Follow the instructions in this [OpenMW news post.](https://openmw.org/2017/distant-terrain/) 214 | 215 | ### Can I make it so players don't share quests on my server? 216 | In theory, yes, by changing a line in your server's *config.lua* from this: 217 | ``` 218 | config.shareJournal = true 219 | ``` 220 | Into this: 221 | ``` 222 | config.shareJournal = false 223 | ``` 224 | However, Morrowind's quest logic and NPC dialogue is built around the existence of a single player. As a result, the vast majority of questlines will break very quickly if their states are not shared across players. 225 | 226 | The ability to disable journal sharing is more appropriate for new worlds or areas built specifically around multiplayer. 227 | --------------------------------------------------------------------------------