diff --git a/404.html b/404.html index 0a44b343..4840019d 100644 --- a/404.html +++ b/404.html @@ -5,13 +5,13 @@ Page Not Found | RogueLibs Documentation - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/assets/js/4b4e775c.cad65240.js b/assets/js/4b4e775c.cece1753.js similarity index 99% rename from assets/js/4b4e775c.cad65240.js rename to assets/js/4b4e775c.cece1753.js index a27f6645..2e4e6389 100644 --- a/assets/js/4b4e775c.cad65240.js +++ b/assets/js/4b4e775c.cece1753.js @@ -1 +1 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[6895],{3905:(e,t,n)=>{n.d(t,{Zo:()=>d,kt:()=>f});var r=n(7294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),u=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},d=function(e){var t=u(e.components);return r.createElement(s.Provider,{value:t},e.children)},c="mdxType",p={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},m=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,d=l(e,["components","mdxType","originalType","parentName"]),c=u(n),m=a,f=c["".concat(s,".").concat(m)]||c[m]||p[m]||o;return n?r.createElement(f,i(i({ref:t},d),{},{components:n})):r.createElement(f,i({ref:t},d))}));function f(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,i=new Array(o);i[0]=m;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[c]="string"==typeof e?e:a,i[1]=l;for(var u=2;u{n.d(t,{Z:()=>o});var r=n(7462),a=n(7294);function o(e){let{children:t,...n}=e;return a.createElement("div",(0,r.Z)({role:"tabpanel"},n),t)}},5878:(e,t,n)=>{n.d(t,{Z:()=>g});var r=n(7294),a=n(6550),o=n(1980),i=n(7392),l=n(12);function s(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:n,attributes:r,default:a}}=e;return{value:t,label:n,attributes:r,default:a}}))}function u(e){const{values:t,children:n}=e;return(0,r.useMemo)((()=>{const e=t??s(n);return function(e){const t=(0,i.l)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,n])}function d(e){let{value:t,tabValues:n}=e;return n.some((e=>e.value===t))}function c(e){let{queryString:t=!1,groupId:n}=e;const i=(0,a.k6)(),l=function(e){let{queryString:t=!1,groupId:n}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:t,groupId:n});return[(0,o._X)(l),(0,r.useCallback)((e=>{if(!l)return;const t=new URLSearchParams(i.location.search);t.set(l,e),i.replace({...i.location,search:t.toString()})}),[l,i])]}function p(e){const{defaultValue:t,queryString:n=!1,groupId:a}=e,o=u(e),[i,s]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:n}=e;if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!d({value:t,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const r=n.find((e=>e.default))??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:t,tabValues:o}))),[p,m]=c({queryString:n,groupId:a}),[f,h]=function(e){let{groupId:t}=e;const n=function(e){return e?`docusaurus.tab.${e}`:null}(t),[a,o]=(0,l.Nk)(n);return[a,(0,r.useCallback)((e=>{n&&o.set(e)}),[n,o])]}({groupId:a}),g=(()=>{const e=p??f;return d({value:e,tabValues:o})?e:null})();(0,r.useLayoutEffect)((()=>{g&&s(g)}),[g]);return{selectedValue:i,selectValue:(0,r.useCallback)((e=>{if(!d({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);s(e),m(e),h(e)}),[m,h,o]),tabValues:o}}var m=n(6010);const f={tabItem:"tabItem_V91s",tabItemActive:"tabItemActive_JsUu",blink:"blink_ZPVS",tab:"tab_ntnM"};const h={left:37,right:39};function g(e){const{lazy:t,defaultValue:n,values:a,groupId:o}=e,i=r.Children.toArray(e.children),{tabValues:l,selectedValue:s,selectValue:u}=p({children:i,defaultValue:n,values:a,groupId:o}),d=[],c=e=>{const t=e.currentTarget,n=a[d.indexOf(t)].value;u(n),null!=o&&setTimeout((()=>{(function(e){const{top:t,left:n,bottom:r,right:a}=e.getBoundingClientRect(),{innerHeight:o,innerWidth:i}=window;return t>=0&&a<=i&&r<=o&&n>=0})(t)||(t.scrollIntoView({block:"center",behavior:"smooth"}),t.classList.add(f.tabItemActive),setTimeout((()=>t.classList.remove(f.tabItemActive)),2e3))}),150)},g=e=>{let t;switch(e.keyCode){case h.right:{const n=d.indexOf(e.target)+1;t=d[n]||d[0];break}case h.left:{const n=d.indexOf(e.target)-1;t=d[n]||d[d.length-1];break}default:return}t.focus()},y=(e,t)=>t.value===e||t.values&&-1!=t.values.indexOf(e);return r.createElement("div",{className:"tabs-container"},r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:"tabs"},a.map((e=>{let{value:t,label:n}=e;return r.createElement("li",{role:"tab",tabIndex:s===t?0:-1,"aria-selected":s===t,className:(0,m.Z)("tabs__item",f.tabItem,{"tabs__item--active":s===t}),key:t,ref:e=>e&&d.push(e),onKeyDown:g,onFocus:c,onClick:c},n)}))),t?r.cloneElement(i.find((e=>y(s,e.props))),{className:f.tab}):r.createElement("div",null,i.map(((e,t)=>r.cloneElement(e,{key:t,hidden:!y(s,e.props),className:f.tab})))),r.createElement("br",null))}},2573:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>u,contentTitle:()=>l,default:()=>m,frontMatter:()=>i,metadata:()=>s,toc:()=>d});var r=n(7462),a=(n(7294),n(3905)),o=(n(5878),n(1016),n(4996));const i={},l="Getting Started",s={unversionedId:"dev/getting-started",id:"dev/getting-started",title:"Getting Started",description:"Welcome to the SoR mod-making guide featuring RogueLibs! Library and tools provided by RogueLibs really simplify the modding process, but you'll still need some basic C# knowledge to get started. If you have any questions, feel free to ask them in the official Discord's #\ud83d\udd27|modding channel.",source:"@site/docs/dev/getting-started.mdx",sourceDirName:"dev",slug:"/dev/getting-started",permalink:"/RogueLibs/docs/dev/getting-started",draft:!1,editUrl:"https://github.com/SugarBarrel/RogueLibs/edit/main/website/docs/dev/getting-started.mdx",tags:[],version:"current",frontMatter:{},sidebar:"documentationSidebar",previous:{title:"Frequently Encountered Problems",permalink:"/RogueLibs/docs/user/troubleshooting"},next:{title:"Creating a Custom Item",permalink:"/RogueLibs/docs/dev/items/create-item"}},u={},d=[{value:"Required software",id:"tools",level:2},{value:"New Way of Modding",id:"new-way-of-modding",level:2},{value:"Workspace Structure",id:"workspace-structure",level:2},{value:".ref - References",id:"references",level:3},{value:".events - PluginBuildEvents",id:"pluginbuildevents",level:3},{value:"Solution Folders",id:"solution-folders",level:3}],c={toc:d},p="wrapper";function m(e){let{components:t,...n}=e;return(0,a.kt)(p,(0,r.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"getting-started"},"Getting Started"),(0,a.kt)("p",null,"Welcome to the SoR mod-making guide featuring RogueLibs! Library and tools provided by RogueLibs really simplify the modding process, but you'll still need some basic C# knowledge to get started. If you have any questions, feel free to ask them in the official Discord's ",(0,a.kt)("a",{parentName:"p",href:"https://discord.gg/m3zuHSwQw2"},"#\ud83d\udd27|modding")," channel."),(0,a.kt)("h2",{id:"tools"},"Required software"),(0,a.kt)("p",null,"First of all, you'll need to install these tools:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},(0,a.kt)("a",{parentName:"strong",href:"https://github.com/dnSpy/dnSpy/releases/latest"},"dnSpy"))," - a .NET assembly editor (and a debugger, but it's way too tedious to make it work for BepInEx and plugins). You're not gonna edit assemblies, just view them to see how the game and/or other plugins work."),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},(0,a.kt)("a",{parentName:"strong",href:"https://visualstudio.microsoft.com/downloads/"},"Visual Studio 2019 Community"))," - the Integrated Development Environment (IDE for short) that you'll be working in.")),(0,a.kt)("h2",{id:"new-way-of-modding"},"New Way of Modding"),(0,a.kt)("p",null,"Instead of creating a project manually, we'll be using a ",(0,a.kt)("strong",{parentName:"p"},"special template")," with a ton of advantages!"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"The template is SDK-style, which means that:",(0,a.kt)("ul",{parentName:"li"},(0,a.kt)("li",{parentName:"ul"},"You'll be able to use most of the features of the latest C# versions!"),(0,a.kt)("li",{parentName:"ul"},"Less messing around with the settings and configurations!"))),(0,a.kt)("li",{parentName:"ul"},"No DLL Hell. All of the references are in a single designated folder!"),(0,a.kt)("li",{parentName:"ul"},"PluginBuildEvents utility will move your mods to BepInEx/plugins automatically!"),(0,a.kt)("li",{parentName:"ul"},"The template contains the base code to quickly start developing your mod!"),(0,a.kt)("li",{parentName:"ul"},"Most of the stuff you could possibly need is already in the template!")),(0,a.kt)("p",null,"You can just copy-paste the template, and start working on your mod in less than a minute!"),(0,a.kt)("h2",{id:"workspace-structure"},"Workspace Structure"),(0,a.kt)("p",null,"First of all, ",(0,a.kt)("strong",{parentName:"p"},(0,a.kt)("a",{parentName:"strong",href:"https://drive.google.com/file/d/1d1FH0Gh7egp7Z4QugsCF4aCE4-NgWD1X/view?usp=sharing"},"download the workspace template"))," and extract the ",(0,a.kt)("inlineCode",{parentName:"p"},"sor-repos")," folder."),(0,a.kt)("admonition",{title:"Pro-tip: Managing repository directories",type:"tip"},(0,a.kt)("p",{parentName:"admonition"},"You should put your repositories close to the root of the drive, so that they have much shorter and more manageable paths, like ",(0,a.kt)("inlineCode",{parentName:"p"},"D:\\sor-repos"),", ",(0,a.kt)("inlineCode",{parentName:"p"},"F:\\rim-repos")," (for Rimworld mods), ",(0,a.kt)("inlineCode",{parentName:"p"},"E:\\uni-repos")," (for university stuff) and etc. This way you'll always know the exact path to your projects, and all errors and warnings regarding the files will be much shorter and will contain less unnecessary information.")),(0,a.kt)("p",null,"Now let's see what this workspace has to offer!"),(0,a.kt)("h3",{id:"references"},(0,a.kt)("inlineCode",{parentName:"h3"},".ref")," - References"),(0,a.kt)("p",null,(0,a.kt)("inlineCode",{parentName:"p"},".ref")," directory will contain all of the references for your mods. There are two kinds of them:"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},(0,a.kt)("em",{parentName:"strong"},"Static references"))," (that is, the ones that aren't updated frequently and mostly remain the same) are stored in the ",(0,a.kt)("inlineCode",{parentName:"p"},"static")," subdirectory. Most of the stuff that you can find in the ",(0,a.kt)("inlineCode",{parentName:"p"},"/StreetsOfRogue_Data/Managed")," directory goes here."),(0,a.kt)("img",{src:(0,o.Z)("/img/setup/ref-static.png"),width:"600"}),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},(0,a.kt)("em",{parentName:"strong"},"Dynamic references"))," (the ones that change often) are ",(0,a.kt)("inlineCode",{parentName:"p"},"Assembly-CSharp.dll")," (that contains the game code) and ",(0,a.kt)("inlineCode",{parentName:"p"},"RogueLibsCore.dll")," (RogueLibs library). They are stored in the ",(0,a.kt)("inlineCode",{parentName:"p"},".ref")," directory itself, so you can update them more easily."),(0,a.kt)("img",{src:(0,o.Z)("/img/setup/ref.png"),width:"600"}),(0,a.kt)("admonition",{title:"Pro-tip: Documentation files",type:"tip"},(0,a.kt)("p",{parentName:"admonition"},"Some references have documentation as a separate file, like ",(0,a.kt)("inlineCode",{parentName:"p"},"RogueLibsCore.xml"),". Make sure that you place it next to the .dll in the same folder. If you do, you'll be able to look up documentation on types and members right in Visual Studio!")),(0,a.kt)("h3",{id:"pluginbuildevents"},(0,a.kt)("inlineCode",{parentName:"h3"},".events")," - PluginBuildEvents"),(0,a.kt)("p",null,"PluginBuildEvents is a simple utility for copying your mods over to the BepInEx/plugins directory. The default project template includes it as a post-build event, so you just need to build your mod, and its file will be automatically moved!"),(0,a.kt)("admonition",{title:"Non-Steam versions of the game",type:"note"},(0,a.kt)("p",{parentName:"admonition"},"If you haven't purchased the Steam version of the game (or if you somehow messed up the Steam's installation path in the registry), then specify the full path to the game's root directory in the properties of your project (right-click on it in the Solution Explorer and select Properties > Build > Events):"),(0,a.kt)("pre",{parentName:"admonition"},(0,a.kt)("code",{parentName:"pre",className:"language-sh"},'"$(SolutionDir)\\..\\.events\\PluginBuildEvents.exe" "$(TargetPath)" "D:\\Games\\Streets of Rogue"\n'))),(0,a.kt)("h3",{id:"solution-folders"},"Solution Folders"),(0,a.kt)("p",null,"All other folders should contain solutions with your projects:"),(0,a.kt)("img",{src:(0,o.Z)("/img/setup/solutions.png"),width:"600"}),(0,a.kt)("p",null,"To create a new one, just copy-paste the template one. You can also modify the template to fit your specific needs."))}m.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[6895],{3905:(e,t,n)=>{n.d(t,{Zo:()=>d,kt:()=>f});var r=n(7294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),u=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},d=function(e){var t=u(e.components);return r.createElement(s.Provider,{value:t},e.children)},c="mdxType",p={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},m=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,d=l(e,["components","mdxType","originalType","parentName"]),c=u(n),m=a,f=c["".concat(s,".").concat(m)]||c[m]||p[m]||o;return n?r.createElement(f,i(i({ref:t},d),{},{components:n})):r.createElement(f,i({ref:t},d))}));function f(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,i=new Array(o);i[0]=m;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[c]="string"==typeof e?e:a,i[1]=l;for(var u=2;u{n.d(t,{Z:()=>o});var r=n(7462),a=n(7294);function o(e){let{children:t,...n}=e;return a.createElement("div",(0,r.Z)({role:"tabpanel"},n),t)}},5878:(e,t,n)=>{n.d(t,{Z:()=>g});var r=n(7294),a=n(6550),o=n(1980),i=n(7392),l=n(12);function s(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:n,attributes:r,default:a}}=e;return{value:t,label:n,attributes:r,default:a}}))}function u(e){const{values:t,children:n}=e;return(0,r.useMemo)((()=>{const e=t??s(n);return function(e){const t=(0,i.l)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,n])}function d(e){let{value:t,tabValues:n}=e;return n.some((e=>e.value===t))}function c(e){let{queryString:t=!1,groupId:n}=e;const i=(0,a.k6)(),l=function(e){let{queryString:t=!1,groupId:n}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:t,groupId:n});return[(0,o._X)(l),(0,r.useCallback)((e=>{if(!l)return;const t=new URLSearchParams(i.location.search);t.set(l,e),i.replace({...i.location,search:t.toString()})}),[l,i])]}function p(e){const{defaultValue:t,queryString:n=!1,groupId:a}=e,o=u(e),[i,s]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:n}=e;if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!d({value:t,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const r=n.find((e=>e.default))??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:t,tabValues:o}))),[p,m]=c({queryString:n,groupId:a}),[f,h]=function(e){let{groupId:t}=e;const n=function(e){return e?`docusaurus.tab.${e}`:null}(t),[a,o]=(0,l.Nk)(n);return[a,(0,r.useCallback)((e=>{n&&o.set(e)}),[n,o])]}({groupId:a}),g=(()=>{const e=p??f;return d({value:e,tabValues:o})?e:null})();(0,r.useLayoutEffect)((()=>{g&&s(g)}),[g]);return{selectedValue:i,selectValue:(0,r.useCallback)((e=>{if(!d({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);s(e),m(e),h(e)}),[m,h,o]),tabValues:o}}var m=n(6010);const f={tabItem:"tabItem_V91s",tabItemActive:"tabItemActive_JsUu",blink:"blink_ZPVS",tab:"tab_ntnM"};const h={left:37,right:39};function g(e){const{lazy:t,defaultValue:n,values:a,groupId:o}=e,i=r.Children.toArray(e.children),{tabValues:l,selectedValue:s,selectValue:u}=p({children:i,defaultValue:n,values:a,groupId:o}),d=[],c=e=>{const t=e.currentTarget,n=a[d.indexOf(t)].value;u(n),null!=o&&setTimeout((()=>{(function(e){const{top:t,left:n,bottom:r,right:a}=e.getBoundingClientRect(),{innerHeight:o,innerWidth:i}=window;return t>=0&&a<=i&&r<=o&&n>=0})(t)||(t.scrollIntoView({block:"center",behavior:"smooth"}),t.classList.add(f.tabItemActive),setTimeout((()=>t.classList.remove(f.tabItemActive)),2e3))}),150)},g=e=>{let t;switch(e.keyCode){case h.right:{const n=d.indexOf(e.target)+1;t=d[n]||d[0];break}case h.left:{const n=d.indexOf(e.target)-1;t=d[n]||d[d.length-1];break}default:return}t.focus()},y=(e,t)=>t.value===e||t.values&&-1!=t.values.indexOf(e);return r.createElement("div",{className:"tabs-container"},r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:"tabs"},a.map((e=>{let{value:t,label:n}=e;return r.createElement("li",{role:"tab",tabIndex:s===t?0:-1,"aria-selected":s===t,className:(0,m.Z)("tabs__item",f.tabItem,{"tabs__item--active":s===t}),key:t,ref:e=>e&&d.push(e),onKeyDown:g,onFocus:c,onClick:c},n)}))),t?r.cloneElement(i.find((e=>y(s,e.props))),{className:f.tab}):r.createElement("div",null,i.map(((e,t)=>r.cloneElement(e,{key:t,hidden:!y(s,e.props),className:f.tab})))),r.createElement("br",null))}},2573:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>u,contentTitle:()=>l,default:()=>m,frontMatter:()=>i,metadata:()=>s,toc:()=>d});var r=n(7462),a=(n(7294),n(3905)),o=(n(5878),n(1016),n(4996));const i={},l="Getting Started",s={unversionedId:"dev/getting-started",id:"dev/getting-started",title:"Getting Started",description:"Welcome to the SoR mod-making guide featuring RogueLibs! Library and tools provided by RogueLibs really simplify the modding process, but you'll still need some basic C# knowledge to get started. If you have any questions, feel free to ask them in the official Discord's #\ud83d\udd27|modding channel.",source:"@site/docs/dev/getting-started.mdx",sourceDirName:"dev",slug:"/dev/getting-started",permalink:"/RogueLibs/docs/dev/getting-started",draft:!1,editUrl:"https://github.com/SugarBarrel/RogueLibs/edit/main/website/docs/dev/getting-started.mdx",tags:[],version:"current",frontMatter:{},sidebar:"documentationSidebar",previous:{title:"Frequently Encountered Problems",permalink:"/RogueLibs/docs/user/troubleshooting"},next:{title:"Creating a Custom Item",permalink:"/RogueLibs/docs/dev/items/create-item"}},u={},d=[{value:"Required software",id:"tools",level:2},{value:"New Way of Modding",id:"new-way-of-modding",level:2},{value:"Workspace Structure",id:"workspace-structure",level:2},{value:".ref - References",id:"references",level:3},{value:".events - PluginBuildEvents",id:"pluginbuildevents",level:3},{value:"Solution Folders",id:"solution-folders",level:3}],c={toc:d},p="wrapper";function m(e){let{components:t,...n}=e;return(0,a.kt)(p,(0,r.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"getting-started"},"Getting Started"),(0,a.kt)("p",null,"Welcome to the SoR mod-making guide featuring RogueLibs! Library and tools provided by RogueLibs really simplify the modding process, but you'll still need some basic C# knowledge to get started. If you have any questions, feel free to ask them in the official Discord's ",(0,a.kt)("a",{parentName:"p",href:"https://discord.gg/m3zuHSwQw2"},"#\ud83d\udd27|modding")," channel."),(0,a.kt)("h2",{id:"tools"},"Required software"),(0,a.kt)("p",null,"First of all, you'll need to install these tools:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},(0,a.kt)("a",{parentName:"strong",href:"https://github.com/dnSpy/dnSpy/releases/latest"},"dnSpy"))," - a .NET assembly editor (and a debugger, but it's way too tedious to make it work for BepInEx and plugins). You're not gonna edit assemblies, just view them to see how the game and/or other plugins work."),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},(0,a.kt)("a",{parentName:"strong",href:"https://visualstudio.microsoft.com/downloads/"},"Visual Studio 2022 Community"))," - the Integrated Development Environment (IDE for short) that you'll be working in.")),(0,a.kt)("h2",{id:"new-way-of-modding"},"New Way of Modding"),(0,a.kt)("p",null,"Instead of creating a project manually, we'll be using a ",(0,a.kt)("strong",{parentName:"p"},"special template")," with a ton of advantages!"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"The template is SDK-style, which means that:",(0,a.kt)("ul",{parentName:"li"},(0,a.kt)("li",{parentName:"ul"},"You'll be able to use most of the features of the latest C# versions!"),(0,a.kt)("li",{parentName:"ul"},"Less messing around with the settings and configurations!"))),(0,a.kt)("li",{parentName:"ul"},"No DLL Hell. All of the references are in a single designated folder!"),(0,a.kt)("li",{parentName:"ul"},"PluginBuildEvents utility will move your mods to BepInEx/plugins automatically!"),(0,a.kt)("li",{parentName:"ul"},"The template contains the base code to quickly start developing your mod!"),(0,a.kt)("li",{parentName:"ul"},"Most of the stuff you could possibly need is already in the template!")),(0,a.kt)("p",null,"You can just copy-paste the template, and start working on your mod in less than a minute!"),(0,a.kt)("h2",{id:"workspace-structure"},"Workspace Structure"),(0,a.kt)("p",null,"First of all, ",(0,a.kt)("strong",{parentName:"p"},(0,a.kt)("a",{parentName:"strong",href:"https://drive.google.com/file/d/1d1FH0Gh7egp7Z4QugsCF4aCE4-NgWD1X/view?usp=sharing"},"download the workspace template"))," and extract the ",(0,a.kt)("inlineCode",{parentName:"p"},"sor-repos")," folder."),(0,a.kt)("admonition",{title:"Pro-tip: Managing repository directories",type:"tip"},(0,a.kt)("p",{parentName:"admonition"},"You should put your repositories close to the root of the drive, so that they have much shorter and more manageable paths, like ",(0,a.kt)("inlineCode",{parentName:"p"},"D:\\sor-repos"),", ",(0,a.kt)("inlineCode",{parentName:"p"},"F:\\rim-repos")," (for Rimworld mods), ",(0,a.kt)("inlineCode",{parentName:"p"},"E:\\uni-repos")," (for university stuff) and etc. This way you'll always know the exact path to your projects, and all errors and warnings regarding the files will be much shorter and will contain less unnecessary information.")),(0,a.kt)("p",null,"Now let's see what this workspace has to offer!"),(0,a.kt)("h3",{id:"references"},(0,a.kt)("inlineCode",{parentName:"h3"},".ref")," - References"),(0,a.kt)("p",null,(0,a.kt)("inlineCode",{parentName:"p"},".ref")," directory will contain all of the references for your mods. There are two kinds of them:"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},(0,a.kt)("em",{parentName:"strong"},"Static references"))," (that is, the ones that aren't updated frequently and mostly remain the same) are stored in the ",(0,a.kt)("inlineCode",{parentName:"p"},"static")," subdirectory. Most of the stuff that you can find in the ",(0,a.kt)("inlineCode",{parentName:"p"},"/StreetsOfRogue_Data/Managed")," directory goes here."),(0,a.kt)("img",{src:(0,o.Z)("/img/setup/ref-static.png"),width:"600"}),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},(0,a.kt)("em",{parentName:"strong"},"Dynamic references"))," (the ones that change often) are ",(0,a.kt)("inlineCode",{parentName:"p"},"Assembly-CSharp.dll")," (that contains the game code) and ",(0,a.kt)("inlineCode",{parentName:"p"},"RogueLibsCore.dll")," (RogueLibs library). They are stored in the ",(0,a.kt)("inlineCode",{parentName:"p"},".ref")," directory itself, so you can update them more easily."),(0,a.kt)("img",{src:(0,o.Z)("/img/setup/ref.png"),width:"600"}),(0,a.kt)("admonition",{title:"Pro-tip: Documentation files",type:"tip"},(0,a.kt)("p",{parentName:"admonition"},"Some references have documentation as a separate file, like ",(0,a.kt)("inlineCode",{parentName:"p"},"RogueLibsCore.xml"),". Make sure that you place it next to the .dll in the same folder. If you do, you'll be able to look up documentation on types and members right in Visual Studio!")),(0,a.kt)("h3",{id:"pluginbuildevents"},(0,a.kt)("inlineCode",{parentName:"h3"},".events")," - PluginBuildEvents"),(0,a.kt)("p",null,"PluginBuildEvents is a simple utility for copying your mods over to the BepInEx/plugins directory. The default project template includes it as a post-build event, so you just need to build your mod, and its file will be automatically moved!"),(0,a.kt)("admonition",{title:"Non-Steam versions of the game",type:"note"},(0,a.kt)("p",{parentName:"admonition"},"If you haven't purchased the Steam version of the game (or if you somehow messed up the Steam's installation path in the registry), then specify the full path to the game's root directory in the properties of your project (right-click on it in the Solution Explorer and select Properties > Build > Events):"),(0,a.kt)("pre",{parentName:"admonition"},(0,a.kt)("code",{parentName:"pre",className:"language-sh"},'"$(SolutionDir)\\..\\.events\\PluginBuildEvents.exe" "$(TargetPath)" "D:\\Games\\Streets of Rogue"\n'))),(0,a.kt)("h3",{id:"solution-folders"},"Solution Folders"),(0,a.kt)("p",null,"All other folders should contain solutions with your projects:"),(0,a.kt)("img",{src:(0,o.Z)("/img/setup/solutions.png"),width:"600"}),(0,a.kt)("p",null,"To create a new one, just copy-paste the template one. You can also modify the template to fit your specific needs."))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.106692fa.js b/assets/js/runtime~main.0c92652f.js similarity index 99% rename from assets/js/runtime~main.106692fa.js rename to assets/js/runtime~main.0c92652f.js index 309e59bb..8744f897 100644 --- a/assets/js/runtime~main.106692fa.js +++ b/assets/js/runtime~main.0c92652f.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,c,d,t,f={},r={};function b(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={exports:{}};return f[e].call(c.exports,c,c.exports,b),c.exports}b.m=f,e=[],b.O=(a,c,d,t)=>{if(!c){var f=1/0;for(i=0;i=t)&&Object.keys(b.O).every((e=>b.O[e](c[o])))?c.splice(o--,1):(r=!1,t0&&e[i-1][2]>t;i--)e[i]=e[i-1];e[i]=[c,d,t]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,d){if(1&d&&(e=this(e)),8&d)return e;if("object"==typeof e&&e){if(4&d&&e.__esModule)return e;if(16&d&&"function"==typeof e.then)return e}var t=Object.create(null);b.r(t);var f={};a=a||[null,c({}),c([]),c(c)];for(var r=2&d&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>f[a]=()=>e[a]));return f.default=()=>e,b.d(t,f),t},b.d=(e,a)=>{for(var c in a)b.o(a,c)&&!b.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,c)=>(b.f[c](e,a),a)),[])),b.u=e=>"assets/js/"+({30:"d478a8be",53:"935f2afb",241:"c4a6e058",543:"2e587a16",797:"4d817568",834:"dd84e040",1191:"7d423a34",1213:"8cc59e48",1364:"20cc05d3",1590:"af25c0ed",1685:"6ed2b388",1795:"21274f4c",1864:"b0921d62",1978:"ac8b7786",2289:"bbe9b593",2535:"814f3328",2542:"ac891a99",2913:"a3323cb7",3085:"1f391b9e",3089:"a6aa9e1f",3206:"f8409a7e",3221:"be849326",3237:"1df93b7f",3265:"aa8a939b",3381:"64348832",3608:"9e4087bc",3945:"993c7636",4013:"01a85c17",4111:"f054baac",4160:"d36fec9a",4279:"362ef963",4505:"f56ca381",4671:"02211cc8",4798:"273e1e6d",4808:"aabeaa47",4912:"238d53d9",4929:"9b829692",5049:"5474dc5d",5333:"c7654def",5399:"635882d5",5457:"dc722bac",5497:"a274ab97",5680:"a500d0e6",5683:"701505e2",6063:"4d350058",6103:"ccc49370",6317:"8b4601a8",6587:"130c8cde",6634:"e04a90b3",6662:"70a97873",6750:"c088f0d3",6806:"04971410",6895:"4b4e775c",7336:"aadc151e",7863:"760c0af8",7918:"17896441",8045:"03932413",8200:"5331a489",8227:"aa660ebe",8269:"d5a21b17",8279:"b30d71e7",8320:"ccaac848",8514:"f3638af1",8610:"6875c492",8741:"9ad7822c",8893:"7e75972a",8993:"eb197566",9159:"444dc3e7",9207:"a517945c",9336:"69e62760",9514:"1be78505",9706:"343ea7d8",9933:"9f4c4439"}[e]||e)+"."+{30:"4506a04d",53:"1d9a65ba",241:"f148471a",543:"9c7b330b",797:"915ec93b",834:"8afad84d",1191:"8aa76aae",1213:"c8eef7ac",1364:"d5612019",1590:"221a4800",1685:"b646879e",1795:"818fc6c9",1864:"2659b7ab",1978:"d101bcf0",2289:"1dd28414",2535:"58b333eb",2542:"76ecbf9a",2913:"244719a9",3085:"09f29c5c",3089:"b4d1db4f",3206:"2289b2a5",3221:"3db2ae8c",3237:"73085b29",3265:"e4f11e50",3381:"d7aacc13",3473:"24738b29",3608:"c14e11b2",3945:"0fb48e95",4013:"ad8cda28",4111:"41fd2395",4160:"11823113",4279:"eaebe0ff",4505:"bcf6fd9b",4671:"6b22d7be",4798:"2dc44c0e",4808:"10835c7c",4912:"5b00be9e",4929:"6ee463bd",4972:"5992257c",5049:"68e96704",5333:"c995983d",5399:"4b1079b1",5457:"6906f7d3",5497:"8a8240d3",5680:"bc7b78ab",5683:"980e3e84",6048:"7d1d6450",6063:"eec9d500",6103:"f57112ec",6317:"d49b34cb",6587:"3eaf1428",6634:"5b8f4f9c",6662:"16a519fd",6750:"eb4a1a74",6806:"5939def0",6895:"cad65240",7336:"04befe03",7863:"5ce9f507",7918:"81030bd3",8045:"0e62c4c5",8200:"b566a0f3",8227:"4b4eb87c",8269:"19c4909c",8279:"b329caee",8320:"9e5eb8cf",8490:"5a2995b5",8514:"ec4a0dbe",8610:"fa3f8dd7",8741:"a6b1ed64",8893:"75eb8715",8993:"cd88d8a0",9159:"99bfd70f",9207:"5b370640",9336:"9dc6fee4",9514:"d84fe320",9706:"00d55455",9933:"ef1351d9"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),d={},t="website:",b.l=(e,a,c,f)=>{if(d[e])d[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var t=d[e];if(delete d[e],r.parentNode&&r.parentNode.removeChild(r),t&&t.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),o&&document.head.appendChild(r)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/RogueLibs/",b.gca=function(e){return e={17896441:"7918",64348832:"3381",d478a8be:"30","935f2afb":"53",c4a6e058:"241","2e587a16":"543","4d817568":"797",dd84e040:"834","7d423a34":"1191","8cc59e48":"1213","20cc05d3":"1364",af25c0ed:"1590","6ed2b388":"1685","21274f4c":"1795",b0921d62:"1864",ac8b7786:"1978",bbe9b593:"2289","814f3328":"2535",ac891a99:"2542",a3323cb7:"2913","1f391b9e":"3085",a6aa9e1f:"3089",f8409a7e:"3206",be849326:"3221","1df93b7f":"3237",aa8a939b:"3265","9e4087bc":"3608","993c7636":"3945","01a85c17":"4013",f054baac:"4111",d36fec9a:"4160","362ef963":"4279",f56ca381:"4505","02211cc8":"4671","273e1e6d":"4798",aabeaa47:"4808","238d53d9":"4912","9b829692":"4929","5474dc5d":"5049",c7654def:"5333","635882d5":"5399",dc722bac:"5457",a274ab97:"5497",a500d0e6:"5680","701505e2":"5683","4d350058":"6063",ccc49370:"6103","8b4601a8":"6317","130c8cde":"6587",e04a90b3:"6634","70a97873":"6662",c088f0d3:"6750","04971410":"6806","4b4e775c":"6895",aadc151e:"7336","760c0af8":"7863","03932413":"8045","5331a489":"8200",aa660ebe:"8227",d5a21b17:"8269",b30d71e7:"8279",ccaac848:"8320",f3638af1:"8514","6875c492":"8610","9ad7822c":"8741","7e75972a":"8893",eb197566:"8993","444dc3e7":"9159",a517945c:"9207","69e62760":"9336","1be78505":"9514","343ea7d8":"9706","9f4c4439":"9933"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,c)=>{var d=b.o(e,a)?e[a]:void 0;if(0!==d)if(d)c.push(d[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var t=new Promise(((c,t)=>d=e[a]=[c,t]));c.push(d[2]=t);var f=b.p+b.u(a),r=new Error;b.l(f,(c=>{if(b.o(e,a)&&(0!==(d=e[a])&&(e[a]=void 0),d)){var t=c&&("load"===c.type?"missing":c.type),f=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+t+": "+f+")",r.name="ChunkLoadError",r.type=t,r.request=f,d[1](r)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,c)=>{var d,t,f=c[0],r=c[1],o=c[2],n=0;if(f.some((a=>0!==e[a]))){for(d in r)b.o(r,d)&&(b.m[d]=r[d]);if(o)var i=o(b)}for(a&&a(c);n{"use strict";var e,a,c,d,t,f={},r={};function b(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={exports:{}};return f[e].call(c.exports,c,c.exports,b),c.exports}b.m=f,e=[],b.O=(a,c,d,t)=>{if(!c){var f=1/0;for(i=0;i=t)&&Object.keys(b.O).every((e=>b.O[e](c[o])))?c.splice(o--,1):(r=!1,t0&&e[i-1][2]>t;i--)e[i]=e[i-1];e[i]=[c,d,t]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,d){if(1&d&&(e=this(e)),8&d)return e;if("object"==typeof e&&e){if(4&d&&e.__esModule)return e;if(16&d&&"function"==typeof e.then)return e}var t=Object.create(null);b.r(t);var f={};a=a||[null,c({}),c([]),c(c)];for(var r=2&d&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>f[a]=()=>e[a]));return f.default=()=>e,b.d(t,f),t},b.d=(e,a)=>{for(var c in a)b.o(a,c)&&!b.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,c)=>(b.f[c](e,a),a)),[])),b.u=e=>"assets/js/"+({30:"d478a8be",53:"935f2afb",241:"c4a6e058",543:"2e587a16",797:"4d817568",834:"dd84e040",1191:"7d423a34",1213:"8cc59e48",1364:"20cc05d3",1590:"af25c0ed",1685:"6ed2b388",1795:"21274f4c",1864:"b0921d62",1978:"ac8b7786",2289:"bbe9b593",2535:"814f3328",2542:"ac891a99",2913:"a3323cb7",3085:"1f391b9e",3089:"a6aa9e1f",3206:"f8409a7e",3221:"be849326",3237:"1df93b7f",3265:"aa8a939b",3381:"64348832",3608:"9e4087bc",3945:"993c7636",4013:"01a85c17",4111:"f054baac",4160:"d36fec9a",4279:"362ef963",4505:"f56ca381",4671:"02211cc8",4798:"273e1e6d",4808:"aabeaa47",4912:"238d53d9",4929:"9b829692",5049:"5474dc5d",5333:"c7654def",5399:"635882d5",5457:"dc722bac",5497:"a274ab97",5680:"a500d0e6",5683:"701505e2",6063:"4d350058",6103:"ccc49370",6317:"8b4601a8",6587:"130c8cde",6634:"e04a90b3",6662:"70a97873",6750:"c088f0d3",6806:"04971410",6895:"4b4e775c",7336:"aadc151e",7863:"760c0af8",7918:"17896441",8045:"03932413",8200:"5331a489",8227:"aa660ebe",8269:"d5a21b17",8279:"b30d71e7",8320:"ccaac848",8514:"f3638af1",8610:"6875c492",8741:"9ad7822c",8893:"7e75972a",8993:"eb197566",9159:"444dc3e7",9207:"a517945c",9336:"69e62760",9514:"1be78505",9706:"343ea7d8",9933:"9f4c4439"}[e]||e)+"."+{30:"4506a04d",53:"1d9a65ba",241:"f148471a",543:"9c7b330b",797:"915ec93b",834:"8afad84d",1191:"8aa76aae",1213:"c8eef7ac",1364:"d5612019",1590:"221a4800",1685:"b646879e",1795:"818fc6c9",1864:"2659b7ab",1978:"d101bcf0",2289:"1dd28414",2535:"58b333eb",2542:"76ecbf9a",2913:"244719a9",3085:"09f29c5c",3089:"b4d1db4f",3206:"2289b2a5",3221:"3db2ae8c",3237:"73085b29",3265:"e4f11e50",3381:"d7aacc13",3473:"24738b29",3608:"c14e11b2",3945:"0fb48e95",4013:"ad8cda28",4111:"41fd2395",4160:"11823113",4279:"eaebe0ff",4505:"bcf6fd9b",4671:"6b22d7be",4798:"2dc44c0e",4808:"10835c7c",4912:"5b00be9e",4929:"6ee463bd",4972:"5992257c",5049:"68e96704",5333:"c995983d",5399:"4b1079b1",5457:"6906f7d3",5497:"8a8240d3",5680:"bc7b78ab",5683:"980e3e84",6048:"7d1d6450",6063:"eec9d500",6103:"f57112ec",6317:"d49b34cb",6587:"3eaf1428",6634:"5b8f4f9c",6662:"16a519fd",6750:"eb4a1a74",6806:"5939def0",6895:"cece1753",7336:"04befe03",7863:"5ce9f507",7918:"81030bd3",8045:"0e62c4c5",8200:"b566a0f3",8227:"4b4eb87c",8269:"19c4909c",8279:"b329caee",8320:"9e5eb8cf",8490:"5a2995b5",8514:"ec4a0dbe",8610:"fa3f8dd7",8741:"a6b1ed64",8893:"75eb8715",8993:"cd88d8a0",9159:"99bfd70f",9207:"5b370640",9336:"9dc6fee4",9514:"d84fe320",9706:"00d55455",9933:"ef1351d9"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),d={},t="website:",b.l=(e,a,c,f)=>{if(d[e])d[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var t=d[e];if(delete d[e],r.parentNode&&r.parentNode.removeChild(r),t&&t.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),o&&document.head.appendChild(r)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/RogueLibs/",b.gca=function(e){return e={17896441:"7918",64348832:"3381",d478a8be:"30","935f2afb":"53",c4a6e058:"241","2e587a16":"543","4d817568":"797",dd84e040:"834","7d423a34":"1191","8cc59e48":"1213","20cc05d3":"1364",af25c0ed:"1590","6ed2b388":"1685","21274f4c":"1795",b0921d62:"1864",ac8b7786:"1978",bbe9b593:"2289","814f3328":"2535",ac891a99:"2542",a3323cb7:"2913","1f391b9e":"3085",a6aa9e1f:"3089",f8409a7e:"3206",be849326:"3221","1df93b7f":"3237",aa8a939b:"3265","9e4087bc":"3608","993c7636":"3945","01a85c17":"4013",f054baac:"4111",d36fec9a:"4160","362ef963":"4279",f56ca381:"4505","02211cc8":"4671","273e1e6d":"4798",aabeaa47:"4808","238d53d9":"4912","9b829692":"4929","5474dc5d":"5049",c7654def:"5333","635882d5":"5399",dc722bac:"5457",a274ab97:"5497",a500d0e6:"5680","701505e2":"5683","4d350058":"6063",ccc49370:"6103","8b4601a8":"6317","130c8cde":"6587",e04a90b3:"6634","70a97873":"6662",c088f0d3:"6750","04971410":"6806","4b4e775c":"6895",aadc151e:"7336","760c0af8":"7863","03932413":"8045","5331a489":"8200",aa660ebe:"8227",d5a21b17:"8269",b30d71e7:"8279",ccaac848:"8320",f3638af1:"8514","6875c492":"8610","9ad7822c":"8741","7e75972a":"8893",eb197566:"8993","444dc3e7":"9159",a517945c:"9207","69e62760":"9336","1be78505":"9514","343ea7d8":"9706","9f4c4439":"9933"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,c)=>{var d=b.o(e,a)?e[a]:void 0;if(0!==d)if(d)c.push(d[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var t=new Promise(((c,t)=>d=e[a]=[c,t]));c.push(d[2]=t);var f=b.p+b.u(a),r=new Error;b.l(f,(c=>{if(b.o(e,a)&&(0!==(d=e[a])&&(e[a]=void 0),d)){var t=c&&("load"===c.type?"missing":c.type),f=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+t+": "+f+")",r.name="ChunkLoadError",r.type=t,r.request=f,d[1](r)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,c)=>{var d,t,f=c[0],r=c[1],o=c[2],n=0;if(f.some((a=>0!==e[a]))){for(d in r)b.o(r,d)&&(b.m[d]=r[d]);if(o)var i=o(b)}for(a&&a(c);n RogueLibs v3.1.0 released! | RogueLibs Documentation - +

RogueLibs v3.1.0 released!

· 3 min read
Abbysssal

Today a new version of RogueLibs v3.1.0 was released!

A brand new localization system will completely replace the vanilla one!

  • Fancy, easy-to-use XML files, for each language separately;
  • Fixed the bug with "E_" prefixes appearing in random places;
  • Custom languages support;
  • Community translations;
  • Automatic updates;
  • Live Reloading!

Locales opened in VSCode

Upgrading/downgrading to XML

On one hand, we lose some of th-, I mean, the one advantage of CSV files:

  • Small size.
    Not as compact, as physically possible, but still, it has a relatively low data-to-size ratio, close to 1. The only non-data characters are commas, the surrounding quotes, double quotes inside of expressions and new lines.

But on the other hand, we gain a ton of advantages of XML:

  • Easy to read.
    Humans can open the file in a text editor, and it will automatically highlight the XML syntax.
  • Easy to edit.
    Modern text editors check for errors in XML, making it almost impossible to corrupt the file.
  • Easy to parse.
    Computers can easily parse XML files with the built-in System.Xml.Serialization (in case of C#). There's no need for any overcomplicated regular expressions used in the game.

Categorizing translations by language

The vanilla game loads all languages at the same time, regardless of what language is selected.

RogueLibs will load only the ones, that are actually used: the selected one and the fallback one (default: English), that will be used in case an entry is missing in the selected language.

This loading strategy allows you to select from thousands of different localizations, without loading all of them at the same time.

Automatic updates

When you launch the game, RogueLibs will download a small "index" file with some metadata, and then it will decide what translations should be updated. Don't worry, it won't check for updates too frequently, just once every hour.

Community translations

Speaking of translations... The vanilla translations are terrible. I'm not sure what the situation is like with other languages, but russian localization is really bad:

  • Some words are taken out of context. "Glass", as in "a glass wall", not as in "a glass of milk".
  • "Chunk Pack" was literally translated as a "pack" of "chunks". I'm not sure how to explain that, but it caused quite a lot of comotion in the russian community.
  • Some words are not translated at all. "Walkie-Talkie" is still a "Walkie-Talkie". What kind of translator were they even using? Every online translator I could find translated it correctly.
  • Some translation lines were shifted a couple of lines up or down, for some reason. As if someone was editing it in Excel.
  • There was also a ton of different styles. Sometimes "—" was used instead of "-", "…" instead of "..." and stuff like that. As if there were at least 3 different translators, one using Word, other one - Excel and another one - Notepad.
  • "Monkey Barrel" → "Обезьяна баррель". What the f***.
  • I spent about 2 days, working from morning to night, to fix all of that.

And that's why RogueLibs will use community translations instead of official ones.

Feel free to contribute by checking the localization files yourself and making any necessary changes. The localization files are located in here. See more info on contributing to the project here.

Live Reloading

When you edit the localization files, the changes are reflected in the game immediately! Neat!

- + \ No newline at end of file diff --git a/blog/2021/08/31/what-i-hate-about-sor/index.html b/blog/2021/08/31/what-i-hate-about-sor/index.html index ded5689d..1a4dc8d4 100644 --- a/blog/2021/08/31/what-i-hate-about-sor/index.html +++ b/blog/2021/08/31/what-i-hate-about-sor/index.html @@ -5,7 +5,7 @@ What I hate about SoR | RogueLibs Documentation - + @@ -17,7 +17,7 @@ DRY principle (Don't Repeat Yourself).

Binary serialization

Comparison of raw binary data and XML-formatted file

Why would you use binary serialization? It's completely unreadable!
Just use XML, JSON or whatever markup language you want, anything but raw binary data!

Classes are used like structs

Matt clearly doesn't understand the difference between reference and value types.

Lack of properties

Instead of constantly setting and updating fields' values in Awake, Start and other methods, you should just use properties. This way you won't have to constantly update fields' values, once you change one other field's value. The code will instantly become more maintainable.

MyStart method sets up fields and is called in every other method

Virtual methods

Instead of putting dozens of empty virtual methods in a base class and overriding them in a couple of derived types, you should use interfaces! That's literally what they were created for.

Empty virtual methods

Lack of events

Instead of checking for conditions of doing something on every frame update, you should use events. That's literally what they were created for. As a bonus, there's no 1-frame delay, that you get from checking conditions every frame. And also, you know, checking event conditions every frame is a little bit excessive and definitely uses way too much CPU.

Giant for loops

A lot of really big and inefficient loops

Why would you try to repeat the operation, if you know that the results will be exactly the same?

An unnecessary for loop

Why the hell would you write all of these giant for loops with a ton of conditions, breaks and continues and stuff like that, when there are already methods that do exactly that?
Just use System.Linq extensions and List<T> methods!

Conclusion

Well, that's it. For now, at least.

I wish Matt would fix all of that and make SoR 2 at least slightly more moddable and maintainable. If all of that stuff gets fixed in SoR 2, I'd be really happy, and I'd definitely make sure that SoR 2 has the best modding library possible. Otherwise, I might just refuse to make mods for the sequel, when it comes out.

- + \ No newline at end of file diff --git a/blog/2022/01/30/roguelibs-v3.5.0-beta/index.html b/blog/2022/01/30/roguelibs-v3.5.0-beta/index.html index fc29bb0a..d16b5e6f 100644 --- a/blog/2022/01/30/roguelibs-v3.5.0-beta/index.html +++ b/blog/2022/01/30/roguelibs-v3.5.0-beta/index.html @@ -5,13 +5,13 @@ RogueLibs v3.5.0 enters beta | RogueLibs Documentation - +

RogueLibs v3.5.0 enters beta

· 2 min read
Abbysssal

Today RogueLibs v3.5.0 enters a long beta, because there's a huge list of stuff that needs to be done before the release, mainly adding new custom interactions without adding more patches. You'll be able to download the beta version on RogueLibs' releases page.

Just like the localization system in v3.1.0, v3.5.0 will completely replace the vanilla interactions system. The original code in these places is absolutely awful and inconsistent. So, we'll have to rewrite every single vanilla interaction to be moddable.

note

In RogueLibs v3.5.0-beta.X all of the objects will have this button. And by "that button" I mean the "I am patched!" one, not "Make Offering of Human Body" one. It will be removed from the v3.5.0 full release, of course.

Click "Read More" to see the entire to-do list.

Agent, ✅AirConditioner, ✅AlarmButton, ✅Altar, ✅AmmoDispenser, ❌ArcadeGame, ❌ATMMachine, ❌AugmentationBooth, ✅Barbecue, ❌BarbedWire, ❌Bars, ❌(?)BarStool, ❌(?)Bathtub, ❌Bed, ❌(?)Boulder, ❌(?)BoulderSmall, ❌(?)Bush, ❌CapsuleMachine, ❌(?)Chair, ❌(?)ChestBasic, ❌CloneMachine, ❌Computer, ❌(?)Counter, ❌Crate, ❌(?)Desk, ❌Door, ❌Elevator, ❌(?)ExplodingBarrel, ❌FireHydrant, ❌(?)FirePlace, ❌(?)FireSpewer, ❌(?)FlameGrate, ❌(?)FlamingBarrel, ❌(?)GasVent, ❌Generator, ❌Generator2, ❌(?)Gravestone, ❌(?)Item, ❌Jukebox, ❌(?)KillerPlant, ❌(?)Lamp, ❌LaserEmitter, ❌LoadoutMachine, ❌(?)LockdownWall, ❌(?)Manhole, ❌(?)MineCart, ❌(?)MovieScreen, ❌PawnShopMachine, ❌(?)Plant, ❌Podium, ❌PoliceBox, ❌(?)PoolTable, ❌PowerBox, ❌Refrigerator, ❌Safe, ❌SatelliteDish, ❌(?)SawBlade, ❌SecurityCam, ❌(?)Shelf, ❌(?)Sign, ❌(?)SlimeBarrel, ❌(?)SlimePuddle, ❌SlotMachine, ❌(?)Speaker, ❌(?)StartingPoint, ❌(?)Stove, ❌(?)SwitchBasic, ❌(?)Table, ❌(?)TableBig, ❌(?)Television, ❌Toilet, ❌(?)Train, ❌TrapDoor, ❌(?)TrashCan, ❌(?)Tree, ❌(?)Tube, ❌Turntables, ❌Turret, ❌(?)VendorCart, ❌(?)WasteBasket, ❌WaterPump, ❌Well, ❌Window.

Legend

❌ - not implemented, 🔹 - not tested, ✅ - implemented, (?) - may be tricky to implement.

See the #modding channel on the official SoR's Discord server for an up-to-date information. The list in this article will not be updated.

- + \ No newline at end of file diff --git a/blog/archive/index.html b/blog/archive/index.html index f73afcea..51d9dafa 100644 --- a/blog/archive/index.html +++ b/blog/archive/index.html @@ -5,13 +5,13 @@ Archive | RogueLibs Documentation - + - + \ No newline at end of file diff --git a/blog/index.html b/blog/index.html index def57bd6..30bf66ba 100644 --- a/blog/index.html +++ b/blog/index.html @@ -5,13 +5,13 @@ Blog | RogueLibs Documentation - +

· 2 min read
Abbysssal

Today RogueLibs v3.5.0 enters a long beta, because there's a huge list of stuff that needs to be done before the release, mainly adding new custom interactions without adding more patches. You'll be able to download the beta version on RogueLibs' releases page.

Just like the localization system in v3.1.0, v3.5.0 will completely replace the vanilla interactions system. The original code in these places is absolutely awful and inconsistent. So, we'll have to rewrite every single vanilla interaction to be moddable.

note

In RogueLibs v3.5.0-beta.X all of the objects will have this button. And by "that button" I mean the "I am patched!" one, not "Make Offering of Human Body" one. It will be removed from the v3.5.0 full release, of course.

Click "Read More" to see the entire to-do list.

· 6 min read
Abbysssal

The code is really hard and wet

.. Wait- No! I mean that the game is really hardcoded! And the code is very WET - it's the opposite of the DRY (Don't Repeat Yourself) principle... It's pretty funny to say that though.

So, anyways, I decided to make a list of things that I hate about modding Streets of Rogue:

· 3 min read
Abbysssal

Today a new version of RogueLibs v3.1.0 was released!

A brand new localization system will completely replace the vanilla one!

  • Fancy, easy-to-use XML files, for each language separately;
  • Fixed the bug with "E_" prefixes appearing in random places;
  • Custom languages support;
  • Community translations;
  • Automatic updates;
  • Live Reloading!

Locales opened in VSCode

- + \ No newline at end of file diff --git a/blog/tags/blog/index.html b/blog/tags/blog/index.html index eb5c4b99..5c36b159 100644 --- a/blog/tags/blog/index.html +++ b/blog/tags/blog/index.html @@ -5,13 +5,13 @@ One post tagged with "blog" | RogueLibs Documentation - +

One post tagged with "blog"

View All Tags

· 6 min read
Abbysssal

The code is really hard and wet

.. Wait- No! I mean that the game is really hardcoded! And the code is very WET - it's the opposite of the DRY (Don't Repeat Yourself) principle... It's pretty funny to say that though.

So, anyways, I decided to make a list of things that I hate about modding Streets of Rogue:

- + \ No newline at end of file diff --git a/blog/tags/index.html b/blog/tags/index.html index bf47d4f1..41dbd3cf 100644 --- a/blog/tags/index.html +++ b/blog/tags/index.html @@ -5,13 +5,13 @@ Tags | RogueLibs Documentation - + - + \ No newline at end of file diff --git a/blog/tags/release/index.html b/blog/tags/release/index.html index 9dde2700..ee0bfc63 100644 --- a/blog/tags/release/index.html +++ b/blog/tags/release/index.html @@ -5,13 +5,13 @@ One post tagged with "release" | RogueLibs Documentation - +

One post tagged with "release"

View All Tags

· 3 min read
Abbysssal

Today a new version of RogueLibs v3.1.0 was released!

A brand new localization system will completely replace the vanilla one!

  • Fancy, easy-to-use XML files, for each language separately;
  • Fixed the bug with "E_" prefixes appearing in random places;
  • Custom languages support;
  • Community translations;
  • Automatic updates;
  • Live Reloading!

Locales opened in VSCode

- + \ No newline at end of file diff --git a/blog/tags/roguelibs/index.html b/blog/tags/roguelibs/index.html index b119ef43..69124f51 100644 --- a/blog/tags/roguelibs/index.html +++ b/blog/tags/roguelibs/index.html @@ -5,13 +5,13 @@ 2 posts tagged with "roguelibs" | RogueLibs Documentation - +

2 posts tagged with "roguelibs"

View All Tags

· 2 min read
Abbysssal

Today RogueLibs v3.5.0 enters a long beta, because there's a huge list of stuff that needs to be done before the release, mainly adding new custom interactions without adding more patches. You'll be able to download the beta version on RogueLibs' releases page.

Just like the localization system in v3.1.0, v3.5.0 will completely replace the vanilla interactions system. The original code in these places is absolutely awful and inconsistent. So, we'll have to rewrite every single vanilla interaction to be moddable.

note

In RogueLibs v3.5.0-beta.X all of the objects will have this button. And by "that button" I mean the "I am patched!" one, not "Make Offering of Human Body" one. It will be removed from the v3.5.0 full release, of course.

Click "Read More" to see the entire to-do list.

· 3 min read
Abbysssal

Today a new version of RogueLibs v3.1.0 was released!

A brand new localization system will completely replace the vanilla one!

  • Fancy, easy-to-use XML files, for each language separately;
  • Fixed the bug with "E_" prefixes appearing in random places;
  • Custom languages support;
  • Community translations;
  • Automatic updates;
  • Live Reloading!

Locales opened in VSCode

- + \ No newline at end of file diff --git a/blog/tags/sor-2/index.html b/blog/tags/sor-2/index.html index 28bbdccc..47d0722f 100644 --- a/blog/tags/sor-2/index.html +++ b/blog/tags/sor-2/index.html @@ -5,13 +5,13 @@ One post tagged with "sor2" | RogueLibs Documentation - +

One post tagged with "sor2"

View All Tags

· 6 min read
Abbysssal

The code is really hard and wet

.. Wait- No! I mean that the game is really hardcoded! And the code is very WET - it's the opposite of the DRY (Don't Repeat Yourself) principle... It's pretty funny to say that though.

So, anyways, I decided to make a list of things that I hate about modding Streets of Rogue:

- + \ No newline at end of file diff --git a/docs/dev/custom-sprites/index.html b/docs/dev/custom-sprites/index.html index d25b8978..7d529960 100644 --- a/docs/dev/custom-sprites/index.html +++ b/docs/dev/custom-sprites/index.html @@ -5,13 +5,13 @@ Custom Sprites and Resources | RogueLibs Documentation - +

Custom Sprites and Resources

Adding image resources to a plugin is a bit tricky. Visual Studio loads any image resources as bitmaps, but we don't want that. You'll need to modify the resources manifest yourself, and add your images as binary files (or byte arrays). RogueLibs supports cropping and resizing your sprites, so, technically, you could even upload a spritesheet, and cut sprites out of it, but the performance gain would not be worth it (loading all of the 1204 64x64 sprites from Sidi's spritepack takes about 350 milliseconds, while patching methods is significantly slower - RogueLibs takes about 5 seconds to load).

Adding binary resources

First of all, encode your images in PNG or JPG format, and audioclips in MP3, OGG or WAV.

Then go to your project's Properties and create a resource file, if it doesn't exist already.

Open your project's Resources.resx in an external editor and add the following element to the end:

  <!-- ... -->
<data name="MyAwesomeSprite" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\MyAwesomeSprite.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>

You only need to change the name attribute and the first part of the value element for your resources. Then you should be able to reference them like this: Properties.Resources.MyAwesomeSprite. You can do the same thing with other types of files as well, like audioclips, fonts and models.

After changing the .resx file, go to your solution and rebuild the resources code by right-clicking on Resources.resx and selecting "Run Custom Tool".

RogueSprite

note

If you're wondering, why it's called RogueSprite and not CustomSprite, like most of the things in RogueLibs (CustomItem, CustomTrait, CustomEffect), well, that's because it behaves slightly differently from others. CustomItem, CustomTrait and etc. are hooks, while RogueSprite, technically, is a collection of hooks. Also, it took an incredible amount of time and work to figure out that TK2D stuff and I just wanted to distinguish it from other classes.

Normally, new instances of RogueSprite are created like this:

RogueLibs.CreateCustomItem<MyCustomItem>()
...
.WithSprite(Properties.Resources.MyCustomItem);

But you can create them directly too, although you'll have to specify a name and a scope. Scope of the sprite determines what areas of the game the sprite will be available in. SpriteScope.Items will work only on items, SpriteScope.Objects - only on objects, and etc. You can specify multiple scopes too by combining them with | operator.

RogueLibs.CreateCustomSprite("name", SpriteScope.Items, Properties.Resources.ResourceName);

By default, RogueLibs uses the entire file as a texture. You can specify the region of the texture to use with a Rect parameter (for example, if you included a color palette used in the image by its side and don't want to display it in the game):

RogueLibs.CreateCustom("name", SpriteScope.Items, Properties.Resources.ResourceName,
new Rect(0f, 0f, 64f, 64f));

If you're going to use non-64x64 textures, then you need to specify the PPU (pixels-per-unit) parameter too:

RogueLibs.CreateCustom("name", SpriteScope.Items, Properties.Resources.ResourceName,
new Rect(64f, 64f, 128f, 128f), 128f);

Sprite variations

If you have more than 1 sprite for your item, use InvItem.LoadItemSprite in your item's SetupDetails:

public class Present : CustomItem, IItemUsable
{
[RLSetup]
public static void Setup()
{
RogueLibs.CreateCustomItem<Present>()
/* ... */
.WithSprite(Properties.Resources.Present);

RogueLibs.CreateCustomSprite("Present2", SpriteScope.Items, Properties.Resources.Present2);
RogueLibs.CreateCustomSprite("Present3", SpriteScope.Items, Properties.Resources.Present3);
}

public override void SetupDetails()
{
/* ... */
int rnd = new Random().Next(3) + 1;
// random integer x, such that 1 ≤ x ≤ 3

if (rnd != 1) // load a different sprite if it's 2 or 3
Item.LoadItemSprite($"Present{rnd}");

// if it's 1, then the default "Present" will be used
}
}

This example works only if an item actually exists. In the Loadout and Rewards menus it will always have the Present sprite. If you want the sprite in the menus to be randomly selected too, you'll have to override the DisplayedUnlock.GetImage method.

- + \ No newline at end of file diff --git a/docs/dev/disasters/create-disaster/index.html b/docs/dev/disasters/create-disaster/index.html index 4c1cbd4e..17d5b3c4 100644 --- a/docs/dev/disasters/create-disaster/index.html +++ b/docs/dev/disasters/create-disaster/index.html @@ -5,7 +5,7 @@ Creating a Custom Disaster | RogueLibs Documentation - + @@ -14,7 +14,7 @@ (0 - Slums, 1 - Industrial, 2 - Park, 3 - Downtown, 4 - Uptown, 5 - Mayor Village)
  • CurrentFloor - index of the current level's floor of the district. (0, 1 or 2 in a normal playthrough, and 0 or 1 with "Quick Game" mutator on)
  • CurrentLevel - index of the current level. (0-2 - Slums, 3-5 - Industrial, 6-8 - Park, 9-11 - Downtown, 12-14 - Uptown, 15 - Mayor Village)(or 0-1, 2-3, 4-5, 6-7, 8-9, 10 if Quick Game is on), and more in Endless mode)
  • And, if you want to force your disaster onto a level, override the TestForced method.

    MyCustomDisaster.cs
    public class MyCustomDisaster : CustomDisaster
    {
    public override bool TestForced()
    {
    // for example, if there's a Mayor on the level
    return gc.agentList.Exists(a => a.agentName === VanillaAgents.Mayor);
    }
    }
    Current limitations

    At the moment, TestForced cannot force a disaster onto a non-disastrous level. It only works on levels that normally have disasters: *-3 (or *-2 with Quick Game on), or every level with the Disasters Every Level mutator.

    Disaster Settings

    Normally, you can't teleport during disasters, but you can change that by overriding the AllowTeleport property:

    MyCustomDisaster.cs
    public class MyCustomDisaster : CustomDisaster
    {
    public override bool AllowTeleport => true;
    }

    The property is accessed constantly, so you can change the return value with time.

    Initialization

    Just call the CreateCustomDisaster method with your disaster's type as a parameter:

    MyCustomDisaster.cs
    public class MyCustomDisaster : CustomDisaster
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomDisaster<MyCustomDisaster>();
    }
    }
    note

    See more about the RLSetup attribute here.

    You can set your disaster's name and description using WithName and WithDescription methods:

    MyCustomDisaster.cs
    public class MyCustomDisaster : CustomDisaster
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomDisaster<MyCustomDisaster>()
    .WithName(new CustomNameInfo("My Custom Disaster"))
    .WithDescription(new CustomNameInfo("My Custom Disaster is very cool and does a lot of great stuff"));
    }
    }

    Plus, you can add two messages (they are displayed at the same time, on two lines):

    MyCustomDisaster.cs
    public class MyCustomDisaster : CustomDisaster
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomDisaster<MyCustomDisaster>()
    .WithName(new CustomNameInfo("My Custom Disaster"))
    .WithDescription(new CustomNameInfo("My Custom Disaster is very cool and does a lot of great stuff"))
    .WithMessage(new CustomNameInfo("My Custom Disaster!"))
    .WithMessage(new CustomNameInfo("Watch out for... uh, something dangerous!"));
    }
    }
    info

    See Custom Names for more info.

    You can also create a removal mutator automatically:

    MyCustomDisaster.cs
    public class MyCustomDisaster : CustomDisaster
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomDisaster<MyCustomDisaster>()
    .WithName(new CustomNameInfo("My Custom Disaster"))
    .WithDescription(new CustomNameInfo("My Custom Disaster is very cool and does a lot of great stuff"))
    .WithMessage(new CustomNameInfo("My Custom Disaster!"))
    .WithMessage(new CustomNameInfo("Watch out for... uh, something dangerous!"))
    .WithRemovalMutator();
    }
    }

    Examples

    A simple disaster that just gives everyone Resurrection after the notification.

    using System.Collections;

    namespace RogueLibsCore.Test
    {
    public class NewHealthOrder : CustomDisaster
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomDisaster<NewHealthOrder>()
    .WithName(new CustomNameInfo
    {
    English = "New Health Order",
    })
    .WithDescription(new CustomNameInfo
    {
    English = "Where is this line used?!",
    })
    .WithMessage(new CustomNameInfo
    {
    English = "N.H.O. - New Health Order",
    })
    .WithMessage(new CustomNameInfo
    {
    English = "Resurrection for everyone!",
    })
    .WithRemovalMutator();
    }

    public override void Start() { }
    public override void Finish() { }

    public override IEnumerator Updating()
    {
    foreach (Agent agent in gc.agentList)
    if (!agent.dead && !agent.electronic && !agent.inhuman)
    {
    agent.statusEffects.AddStatusEffect(VanillaEffects.Resurrection, false);
    }
    yield break;
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/extra/index.html b/docs/dev/extra/index.html index 8e114c78..e88d3469 100644 --- a/docs/dev/extra/index.html +++ b/docs/dev/extra/index.html @@ -5,13 +5,13 @@ Extra Stuff | RogueLibs Documentation - +

    Extra Stuff

    RogueLibs provides a couple of extra interfaces that you can use for any supported custom content hooks: IDoUpdate, IDoFixedUpdate. These interfaces correspond to the Update and FixedUpdate Unity methods (not directly, but through the game's Updater class).

    IDoUpdate and IDoFixedUpdate

    If you want to update your items/traits/effects/objects with Unity's Update or FixedUpdate, implement these interfaces:

    public class MyCustomItem : CustomItem, IDoUpdate
    {
    public void Update()
    {
    /* ... */
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/getting-started/index.html b/docs/dev/getting-started/index.html index 547b6e25..f22623ef 100644 --- a/docs/dev/getting-started/index.html +++ b/docs/dev/getting-started/index.html @@ -5,13 +5,13 @@ Getting Started | RogueLibs Documentation - +
    -

    Getting Started

    Welcome to the SoR mod-making guide featuring RogueLibs! Library and tools provided by RogueLibs really simplify the modding process, but you'll still need some basic C# knowledge to get started. If you have any questions, feel free to ask them in the official Discord's #🔧|modding channel.

    Required software

    First of all, you'll need to install these tools:

    • dnSpy - a .NET assembly editor (and a debugger, but it's way too tedious to make it work for BepInEx and plugins). You're not gonna edit assemblies, just view them to see how the game and/or other plugins work.
    • Visual Studio 2019 Community - the Integrated Development Environment (IDE for short) that you'll be working in.

    New Way of Modding

    Instead of creating a project manually, we'll be using a special template with a ton of advantages!

    • The template is SDK-style, which means that:
      • You'll be able to use most of the features of the latest C# versions!
      • Less messing around with the settings and configurations!
    • No DLL Hell. All of the references are in a single designated folder!
    • PluginBuildEvents utility will move your mods to BepInEx/plugins automatically!
    • The template contains the base code to quickly start developing your mod!
    • Most of the stuff you could possibly need is already in the template!

    You can just copy-paste the template, and start working on your mod in less than a minute!

    Workspace Structure

    First of all, download the workspace template and extract the sor-repos folder.

    Pro-tip: Managing repository directories

    You should put your repositories close to the root of the drive, so that they have much shorter and more manageable paths, like D:\sor-repos, F:\rim-repos (for Rimworld mods), E:\uni-repos (for university stuff) and etc. This way you'll always know the exact path to your projects, and all errors and warnings regarding the files will be much shorter and will contain less unnecessary information.

    Now let's see what this workspace has to offer!

    .ref - References

    .ref directory will contain all of the references for your mods. There are two kinds of them:

    Static references (that is, the ones that aren't updated frequently and mostly remain the same) are stored in the static subdirectory. Most of the stuff that you can find in the /StreetsOfRogue_Data/Managed directory goes here.

    Dynamic references (the ones that change often) are Assembly-CSharp.dll (that contains the game code) and RogueLibsCore.dll (RogueLibs library). They are stored in the .ref directory itself, so you can update them more easily.

    Pro-tip: Documentation files

    Some references have documentation as a separate file, like RogueLibsCore.xml. Make sure that you place it next to the .dll in the same folder. If you do, you'll be able to look up documentation on types and members right in Visual Studio!

    .events - PluginBuildEvents

    PluginBuildEvents is a simple utility for copying your mods over to the BepInEx/plugins directory. The default project template includes it as a post-build event, so you just need to build your mod, and its file will be automatically moved!

    Non-Steam versions of the game

    If you haven't purchased the Steam version of the game (or if you somehow messed up the Steam's installation path in the registry), then specify the full path to the game's root directory in the properties of your project (right-click on it in the Solution Explorer and select Properties > Build > Events):

    "$(SolutionDir)\..\.events\PluginBuildEvents.exe" "$(TargetPath)" "D:\Games\Streets of Rogue"

    Solution Folders

    All other folders should contain solutions with your projects:

    To create a new one, just copy-paste the template one. You can also modify the template to fit your specific needs.

    - +

    Getting Started

    Welcome to the SoR mod-making guide featuring RogueLibs! Library and tools provided by RogueLibs really simplify the modding process, but you'll still need some basic C# knowledge to get started. If you have any questions, feel free to ask them in the official Discord's #🔧|modding channel.

    Required software

    First of all, you'll need to install these tools:

    • dnSpy - a .NET assembly editor (and a debugger, but it's way too tedious to make it work for BepInEx and plugins). You're not gonna edit assemblies, just view them to see how the game and/or other plugins work.
    • Visual Studio 2022 Community - the Integrated Development Environment (IDE for short) that you'll be working in.

    New Way of Modding

    Instead of creating a project manually, we'll be using a special template with a ton of advantages!

    • The template is SDK-style, which means that:
      • You'll be able to use most of the features of the latest C# versions!
      • Less messing around with the settings and configurations!
    • No DLL Hell. All of the references are in a single designated folder!
    • PluginBuildEvents utility will move your mods to BepInEx/plugins automatically!
    • The template contains the base code to quickly start developing your mod!
    • Most of the stuff you could possibly need is already in the template!

    You can just copy-paste the template, and start working on your mod in less than a minute!

    Workspace Structure

    First of all, download the workspace template and extract the sor-repos folder.

    Pro-tip: Managing repository directories

    You should put your repositories close to the root of the drive, so that they have much shorter and more manageable paths, like D:\sor-repos, F:\rim-repos (for Rimworld mods), E:\uni-repos (for university stuff) and etc. This way you'll always know the exact path to your projects, and all errors and warnings regarding the files will be much shorter and will contain less unnecessary information.

    Now let's see what this workspace has to offer!

    .ref - References

    .ref directory will contain all of the references for your mods. There are two kinds of them:

    Static references (that is, the ones that aren't updated frequently and mostly remain the same) are stored in the static subdirectory. Most of the stuff that you can find in the /StreetsOfRogue_Data/Managed directory goes here.

    Dynamic references (the ones that change often) are Assembly-CSharp.dll (that contains the game code) and RogueLibsCore.dll (RogueLibs library). They are stored in the .ref directory itself, so you can update them more easily.

    Pro-tip: Documentation files

    Some references have documentation as a separate file, like RogueLibsCore.xml. Make sure that you place it next to the .dll in the same folder. If you do, you'll be able to look up documentation on types and members right in Visual Studio!

    .events - PluginBuildEvents

    PluginBuildEvents is a simple utility for copying your mods over to the BepInEx/plugins directory. The default project template includes it as a post-build event, so you just need to build your mod, and its file will be automatically moved!

    Non-Steam versions of the game

    If you haven't purchased the Steam version of the game (or if you somehow messed up the Steam's installation path in the registry), then specify the full path to the game's root directory in the properties of your project (right-click on it in the Solution Explorer and select Properties > Build > Events):

    "$(SolutionDir)\..\.events\PluginBuildEvents.exe" "$(TargetPath)" "D:\Games\Streets of Rogue"

    Solution Folders

    All other folders should contain solutions with your projects:

    To create a new one, just copy-paste the template one. You can also modify the template to fit your specific needs.

    + \ No newline at end of file diff --git a/docs/dev/hooks/hook-factories/index.html b/docs/dev/hooks/hook-factories/index.html index ba3defc5..b9e7fcfb 100644 --- a/docs/dev/hooks/hook-factories/index.html +++ b/docs/dev/hooks/hook-factories/index.html @@ -5,13 +5,13 @@ Hook Factories | RogueLibs Documentation - +

    Hook Factories

    Hook factories are responsible for the creation of hooks. Custom content classes use hook factories internally, so there's basically no need for you to create your own. But, if you want to semi-automate the creation of a lot of different items/traits with slightly differing functionality, you can create a class, that will handle everything, and turn its parts on/off conditionally after initializing the hook.

    IHookFactory interface

    You can create hook factories by deriving either from IHookFactory<T> or from HookFactoryBase<T>. The hook factory in the example below will attach a new MyCustomHook hook to all items that have a "Food" category:

    MyCustomHookFactory.cs
    public class MyCustomHookFactory : HookFactoryBase<InvItem>
    {
    public override bool TryCreate(InvItem instance, out IHook<InvItem> hook)
    {
    if (instance.Categories.Contains("Food"))
    {
    hook = new MyCustomHook();
    return true;
    }
    hook = null;
    return false;
    }
    }
    caution

    You only need to create a hook object. Don't attach it to the instance. That's the external code's responsibility.

    if (factory.TryCreate(item, out IHook<InvItem> hook))
    item.AddHook(hook);

    Initialization

    You can either implement your own way of using factories or add it to the RogueLibs' RogueFramework class:

    RogueFramework.ItemFactories.Add(new MyCustomHookFactory());

    Examples

    RogueLibs uses CustomItemFactory and other similar classes to initialize custom items and other hooks:

    public sealed class CustomItemFactory : HookFactoryBase<InvItem>
    {
    private readonly Dictionary<string, ItemEntry> itemsDict = new Dictionary<string, ItemEntry>();

    public override bool TryCreate(InvItem instance, out IHook<InvItem> hook)
    {
    if (instance != null && itemsDict.TryGetValue(instance.invItemName, out ItemEntry entry))
    {
    hook = entry.Initializer();
    if (hook is CustomItem custom)
    custom.ItemInfo = entry.ItemInfo;
    return true;
    }
    hook = null;
    return false;
    }
    public ItemInfo AddItem<TItem>() where TItem : CustomItem, new()
    {
    ItemInfo info = ItemInfo.Get<TItem>();
    itemsDict.Add(info.Name, new ItemEntry { Initializer = () => new TItem(), ItemInfo = info });
    return info;
    }

    private struct ItemEntry
    {
    public Func<IHook<InvItem>> Initializer;
    public ItemInfo ItemInfo;
    }
    }
    public static void InvItem_SetupDetails(InvItem __instance)
    {
    foreach (IHookFactory<InvItem> factory in RogueFramework.ItemFactories)
    if (factory.TryCreate(__instance, out IHook<InvItem> hook))
    {
    __instance.AddHook(hook);
    }
    }
    - + \ No newline at end of file diff --git a/docs/dev/hooks/index.html b/docs/dev/hooks/index.html index 0c2cc28a..5eabeeed 100644 --- a/docs/dev/hooks/index.html +++ b/docs/dev/hooks/index.html @@ -5,13 +5,13 @@ Hooks | RogueLibs Documentation - +

    Hooks

    A hook is an object that is attached to another object ("hook" is a really vague word, jsyk). In RogueLibs hook types derive from IHook and IHook<T>, and RogueLibs provides a mechanism to attach these hooks to vanilla types, such as InvItem, PlayfieldObject, Unlock, Trait and etc. Most custom content classes are based on hooks in one way or another.

    IHook interface

    RogueLibsPatcher.dll creates fields called __RogueLibsHooks in all hookable types. An instance of IHookController class is then assigned to the __RogueLibsHooks field to manage the attached hooks. It provides methods to get, attach and detach hooks from the current instance. Think of it as a collection of hooks.

    You can create your own hooks by deriving either from IHook<T> or from HookBase<T>:

    MyCustomHook.cs
    public class MyCustomHook : HookBase<InvItem>
    {
    protected override void Initialize() { }

    public void StoreInfo(string data)
    {
    Debug.Log($"Stored {data}.");
    Data = data;
    }
    public string LoadInfo()
    {
    Debug.Log($"Loaded {Data}.");
    return Data;
    }
    private string Data;
    }

    Usage

    You can use hooks to store various stuff:

    MyCustomHook hook = item.AddHook<MyCustomHook>();
    hook.StoreInfo("some-information");

    Then you can use that stuff somewhere else:

    MyCustomHook hook = item.GetHook<MyCustomHook>();
    if (hook != null)
    {
    string data = hook.LoadInfo();
    }

    You can attach more than one hook of a type too:

    MyCustomHook hook = item.AddHook<MyCustomHook>();
    hook.StoreInfo("some-information");
    hook = item.AddHook<MyCustomHook>();
    hook.StoreInfo("some-other-stuff");
    hook = item.AddHook<MyCustomHook>();
    hook.StoreInfo("something-else");
    foreach (MyCustomHook hook in item.GetHooks<MyCustomHook>())
    {
    string data = hook.LoadInfo();
    }
    Pro-tip: Hook Factories

    If you want to attach hooks to instances right when they are initialized, use Hook Factories.

    Examples

    info

    Custom content classes (CustomItem, CustomTrait, CustomEffect, CustomAbility and others) are hooks, by the way. You can see the custom classes' implementation in RogueLibs' source code.

    A great example with custom hooks keeping track of seasoned items.

    See the combinable item example here.

    using UnityEngine;

    namespace RogueLibsCore.Test
    {
    [ItemCategories(RogueCategories.Food, RogueCategories.Health)]
    public class SpiceRack : CustomItem, IItemCombinable
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<SpiceRack>()
    .WithName(new CustomNameInfo("Spice Rack"))
    .WithDescription(new CustomNameInfo("Combine with any food item to increase its healing properties."))
    .WithSprite(Properties.Resources.SpiceRack)
    .WithUnlock(new ItemUnlock
    {
    UnlockCost = 10,
    LoadoutCost = 3,
    CharacterCreationCost = 2,
    Prerequisites = { VanillaItems.FoodProcessor },
    });

    SeasonCursorText = RogueLibs.CreateCustomName("SeasonItem", NameTypes.Interface, new CustomNameInfo("Season"));
    }
    private static CustomName SeasonCursorText = null!;

    public override void SetupDetails()
    {
    Item.itemType = ItemTypes.Combine;
    Item.itemValue = 4;
    Item.initCount = 10;
    Item.rewardCount = 15;
    Item.stackable = true;
    Item.hasCharges = true;
    }
    public bool CombineFilter(InvItem other)
    {
    if (other.itemType != ItemTypes.Food || other.healthChange is 0
    || !other.Categories.Contains(RogueCategories.Food)) return false;

    SpicedHook? hook = other.GetHook<SpicedHook>();
    return hook is null || hook.Spiciness < 3;
    }
    public bool CombineItems(InvItem other)
    {
    if (!CombineFilter(other)) return false;

    SpicedHook hook = other.GetHook<SpicedHook>() ?? other.AddHook<SpicedHook>();
    hook.IncreaseSpiciness();

    Count--;
    gc.audioHandler.Play(Owner, VanillaAudio.CombineItem);
    return true;
    }
    public CustomTooltip CombineCursorText(InvItem other) => SeasonCursorText;
    public CustomTooltip CombineTooltip(InvItem other)
    {
    if (!CombineFilter(other)) return default;

    SpicedHook? hook = other.GetHook<SpicedHook>();
    int bonus = hook is null ? (int)Mathf.Ceil(other.healthChange / 4f) : hook.HealthBonus;
    return new CustomTooltip($"+{bonus}", Color.green);
    }

    private class SpicedHook : HookBase<InvItem>
    {
    protected override void Initialize()
    => HealthBonus = (int)Mathf.Ceil(Instance.healthChange / 4f);

    public int HealthBonus { get; private set; }
    public int Spiciness { get; private set; }

    public void IncreaseSpiciness()
    {
    if (Spiciness is 3) return;

    Spiciness++;
    Instance.healthChange += HealthBonus;
    }
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/interactions/create-interaction/index.html b/docs/dev/interactions/create-interaction/index.html index dd038b4b..090c4587 100644 --- a/docs/dev/interactions/create-interaction/index.html +++ b/docs/dev/interactions/create-interaction/index.html @@ -5,14 +5,14 @@ Creating a Custom Interaction | RogueLibs Documentation - +

    Creating a Custom Interaction

    RogueLibs v3.5.0 introduced custom interactions with a pretty unique syntax. All of the code is condensed and basically can be put into a single method. You just need to keep in mind that the main function must be pure (mustn't do anything, except add/remove buttons and set callbacks), and that all of the side effects must be set using SetSideEffect or SetStopCallback.

    Using SimpleInteractionProvider class

    The simplest way to create custom interactions is by using a SimpleInteractionProvider class. It allows you to utilize all of the object-oriented programming principles and keeps the code simple and straightforward. Use RogueInteractions.CreateProvider<T> methods to create instances of that class. You can add buttons using h.AddButton inside the handler.

    RogueInteractions.CreateProvider<Crate>(static h => /* h - handler */
    {
    // If interacted with via hacking, do not add the button
    if (h.Helper.interactingFar) return;

    InvItem crateOpener = h.Agent.inventory.FindItem("CrateOpener");
    if (crateOpener is not null)
    {
    // Add the button with a name "UseCrateOpener", with " (<count>) -1" string added to the end
    string extra = $" ({crateOpener.invItemCount}) -1";
    h.AddButton("UseCrateOpener", extra, static m => /* m - interaction model */
    {
    m.Agent.inventory.SubtractFromItemCount(m.Agent.inventory.FindItem("CrateOpener"), 1);
    m.Object.UnlockCrate();
    m.Object.ShowChest();
    });
    }
    });

    // Don't forget to add the localization string for "UseCrateOpener"
    RogueLibs.CreateCustomName("UseCrateOpener", NameTypes.Interface,
    new CustomNameInfo("Use Crate Opener"));
    Handler purity

    Handler methods must be pure, that is, they shouldn't make any observable changes. All of the logic must be contained in buttons, stop callbacks and side effects.

    If you need something to happen immediately after interacting with something, use side effects. DO NOT write that kind of logic in the interaction provider, because it's also used to determine whether an object is interactable and gets called a lot.

    If you have complicated logic with buttons, you can delegate their actions to local or declared methods:

    RogueInteractions.CreateProvider<Crate>(static h =>
    {
    static void UseCrateOpener(InteractionModel<Crate> model)
    {
    /* ... */
    }

    h.AddButton("UseCrateOpener", UseCrateOpener);
    });

    By specifying a type parameter to the method (like CreateProvider<Crate>), it will narrow down the type of objects that you want to add interactions to. If your action may affect multiple types of objects, you can use the more general CreateProvider method, that is triggered on all kinds of objects.

    RogueInteractions.CreateProvider(static h =>
    {
    if (h.Object is Crate)
    h.AddButton("UseCrateOpener", static m => { /* ... */ });
    else if (h.Object is Safe)
    h.AddButton("UseSafeOpener", static m => { /* ... */ });
    else if (h.Object is Agent)
    h.AddButton("UseSkullOpener", static m => { /* ... */ });
    });
    Note the staticity of the lambdas

    It is important that you do not reference h or other variables inside button actions, as they are called in different phases of the interaction process (an exception will be thrown). I recommend using the static keyword when writing lambda expressions to avoid that.

    Implicit Buttons

    Sometimes buttons represent interactions so obvious that you don't want the player to explicitly press them. For example, doors. It would be a nuisance to press "Open" every time you interact with the door. An implicit button is pressed automatically if it's the only button in the menu; otherwise, it acts as a regular button.

    RogueInteractions.CreateProvider<Crate>(static h =>
    {
    h.AddImplicitButton("InspectWeirdCrate", static m =>
    {
    /* ... */
    m.Agent.SayDialogue("InspectWeirdCrate");
    });

    if (h.Agent.inventory.HasItem("CrateOpener"))
    {
    h.AddButton("UseCrateOpener", static m => { /* ... */ });
    }
    });

    If the player doesn't have a Crate Opener, the "InspectWeirdCrate" button will be pressed immediately, without even showing the buttons. If the player has a Crate Opener though, a menu with 2 buttons will pop up (2, not counting the "Done" button).

    Stopping the interaction

    If your interaction failed miserably, not allowing the player to press any other buttons, or if you just want the player to go do something else right after this interaction, use m.StopInteraction().

    RogueInteractions.CreateProvider<Crate>(static h =>
    {
    if (h.Helper.interactingFar) return;

    if (h.Agent.HasTrait("CrateBomber"))
    {
    h.AddButton("TriggerBomb", static m =>
    {
    m.gc.spawnerMain.SpawnExplosion(m.Object, m.Object.tr.position, "Big");
    m.StopInteraction();
    });
    }
    });
    Forcibly stopping the interaction

    There's also an overload accepting a forced: bool parameter. By default, the interaction stop is delayed until after all of the interactions and side effects are executed. If you pass true as the argument, the interaction will be stopped by the time StopInteraction(true) returns.

    Use it when opening another menu or redirecting the interaction to a different object.

    Stop Callbacks

    If your specific interaction failed, but some other interactions from other mods might still work, use stop callbacks. They are called only if there are no other buttons in the menu, or if the interaction is stopped using StopInteraction.

    Stop callbacks are usually used to relay information as to why the interaction was unsuccessful.

    RogueInteractions.CreateProvider<Crate>(static h =>
    {
    if (h.Helper.interactingFar) return;

    if (!h.Agent.inventory.HasItem("CrateOpener"))
    {
    h.SetStopCallback(static m =>
    {
    m.gc.audioHandler.Play(m.Agent, "CantDo");
    m.Agent.SayDialogue("NeedCrateOpener");
    });
    }
    /* ... */
    });
    Overriding stop callbacks

    By default, SetStopCallback overrides any previously defined stop callbacks. If you want to combine your stop callback with any previously defined ones, use CombineStopCallback.

    Side Effects

    Sometimes you want something to happen right after interacting with the object. For example, make the interacted agent react to you interacting with them, or make a bomb explode in your face when you touch it. Side effects are called right after the buttons are set up, but before stop callbacks. So, side effects get called even if the interaction failed or if there are no available buttons.

    RogueInteractions.CreateProvider<Crate>(static h =>
    {
    // Make the interacting agent say something right after interacting
    // with the crate, even if they don't have the Crate Opener.
    h.SetSideEffect(static m => m.Agent.SayDialogue("DialogueWeirdCrate"));

    if (h.Agent.inventory.HasItem("CrateOpener"))
    {
    h.AddButton("UseCrateOpener", static m =>
    {
    /* ... */
    });
    }
    });
    Overriding side effects

    By default, SetSideEffect overrides any previously defined side effects. If you want to combine your side effect with any previously defined ones, use CombineSideEffect.

    Manipulating buttons

    The SimpleInteractionProvider class contains HasButton and RemoveButton methods.
    Use them to augment or modify vanilla or other mods' interactions.

    RogueInteractions.CreateProvider<Door>(static h =>
    {
    if (h.Agent.HasTrait("KeyIlliterate"))
    {
    if (h.HasButton("UseKey"))
    {
    h.RemoveButton("UseKey");
    h.SetStopCallback(static m => m.Agent.SayDialogue("IlliterateCantUseKeys"));
    }
    if (h.HasButton("UseSkeletonKey"))
    {
    h.RemoveButton("UseSkeletonKey");
    h.SetStopCallback(static m => m.Agent.SayDialogue("IlliterateCantUseKeys"));
    }
    }
    });

    Examples

    You can find a ton of examples here (RogueLibs' source code, reimplementing the vanilla interactions).

    - + \ No newline at end of file diff --git a/docs/dev/items/abilities/chargeable-abilities/index.html b/docs/dev/items/abilities/chargeable-abilities/index.html index cbda1a2e..769f5f62 100644 --- a/docs/dev/items/abilities/chargeable-abilities/index.html +++ b/docs/dev/items/abilities/chargeable-abilities/index.html @@ -5,13 +5,13 @@ Chargeable Abilities | RogueLibs Documentation - +

    Chargeable Abilities

    Custom abilities can be made chargeable by implementing the IAbilityChargeable interface. Ability's Count here works as the amount of stored up energy/charges. This interface makes use of some of the game's charging mechanics, but it doesn't completely rely on it. I'd recommend taking a look at Recharging Items, if you need finer control.

    Making abilities chargeable

    Just implement the IAbilityChargeable interface in your ability's class:

    MyChargeableAbility.cs
    public class MyChargeableAbility : CustomAbility, IAbilityChargeable
    {
    public void OnHeld(AbilityHeldArgs e) { /* ... */ }
    public void OnReleased(AbilityReleasedArgs e) { /* ... */ }
    }

    OnHeld is called every frame (I think?) that the special ability button is held. OnReleased is called on the frame that the special ability button is released. Use these in tandem with OnPressed to charge your ability and stuff.

    caution

    AbilityHeldArgs.Interrupt() method is still work-in-progress.

    Examples

    using UnityEngine;

    namespace RogueLibsCore.Test
    {
    public class Kamikaze : CustomAbility, IAbilityChargeable, IDoUpdate
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomAbility<Kamikaze>()
    .WithName(new CustomNameInfo("Kamikaze"))
    .WithDescription(new CustomNameInfo("Charge up and explode everything around you."))
    .WithSprite(Properties.Resources.Kamikaze)
    .WithUnlock(new AbilityUnlock { UnlockCost = 20, CharacterCreationCost = 20 });
    }

    public float Charge { get; private set; }
    public bool IsCharging { get; private set; }

    public override void OnAdded() { }
    public override void OnPressed()
    {
    IsCharging = true;
    gc.audioHandler.Play(Owner, VanillaAudio.GeneratorHiss);
    Owner!.objectMult.chargingSpecialLunge = true;
    }
    public override CustomTooltip GetCountString()
    {
    if (Charge is 0) return default;
    string text = $"{Charge:#.#}s";
    Color color = Color.Lerp(Color.white, Color.red, Charge / 10f);
    if (Charge > 10f)
    {
    text = "BOOM!";
    color = Color.Lerp(Color.white, Color.red, Mathf.PingPong(Time.time * 5, 1f));
    }
    return new CustomTooltip(text, color);
    }
    public void OnHeld(AbilityHeldArgs e)
    {
    Charge += Time.deltaTime;
    e.HeldTime = Charge;
    if (Charge > 10f)
    {
    Owner!.objectMult.chargingSpecialLunge = true;
    }
    }
    public void OnReleased(AbilityReleasedArgs e)
    {
    IsCharging = false;
    Owner!.objectMult.chargingSpecialLunge = false;
    if (e.HeldTime > 10f)
    {
    Owner.AddEffect(VanillaEffects.Resurrection, new CreateEffectInfo(1) { DontShowText = true, IgnoreElectronic = true });
    gc.spawnerMain.SpawnExplosion(Owner, Owner.tr.position, "Huge", false, -1, false, true).noOwnCheck = true;
    Charge = 0f;
    }
    gc.audioHandler.Stop(Owner, VanillaAudio.GeneratorHiss);
    }
    public void Update()
    {
    if (!IsCharging) Charge = Mathf.Max(Charge - Time.deltaTime * 5f, 0f);
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/items/abilities/create-ability/index.html b/docs/dev/items/abilities/create-ability/index.html index 390f7869..e85b524f 100644 --- a/docs/dev/items/abilities/create-ability/index.html +++ b/docs/dev/items/abilities/create-ability/index.html @@ -5,13 +5,13 @@ Creating a Custom Ability | RogueLibs Documentation - +

    Creating a Custom Ability

    Special abilities in SoR are actually implemented as items. They have SetupDetails, Count, and exist in the owner's inventory, just like items. The CustomAbility class provided by RogueLibs inherits from the CustomItem class and provides a default implementation of SetupDetails. Just like with custom items, you can use interfaces to expand your ability's functionality: IAbilityRechargeable, IAbilityChargeable, IAbilityTargetable.

    CustomAbility class

    To make a custom ability, you need to create a class deriving from CustomAbility:

    MyCustomAbility.cs
    public class MyCustomAbility : CustomAbility
    {
    /* ... */
    }

    You only need to implement 2 methods: OnAdded is called when a character receives this special ability, and OnPressed is called when the player uses the ability. There's no OnRemoved at the moment, because it's not implemented in SoR.

    MyCustomAbility.cs
    public class MyCustomAbility : CustomAbility
    {
    public override void OnAdded() { /* ... */ }
    public override void OnPressed() { /* ... */ }
    }

    SetupDetails

    SetupDetails is overriden by CustomAbility and here's its implementation:

        public override void SetupDetails()
    {
    Item.stackable = true;
    Item.initCount = 0;
    Item.lowCountThreshold = 100;
    }

    This method should work for most abilities, but if you need something more sophisticated, then override it yourself.

    Initialization

    Just call the CreateCustomAbility method with your ability's type as a parameter:

    MyCustomAbility.cs
    public class MyCustomAbility : CustomAbility
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomAbility<MyCustomAbility>();
    }
    }
    note

    See more about the RLSetup attribute here.

    You can set your ability's name and description using WithName and WithDescription methods:

    MyCustomAbility.cs
    public class MyCustomAbility : CustomAbility
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomAbility<MyCustomAbility>()
    .WithName(new CustomNameInfo("My Custom Ability"))
    .WithDescription(new CustomNameInfo("My Custom Ability is very cool and does a lot of great stuff"));
    }
    }

    You can do the same with sprites and unlocks:

    MyCustomAbility.cs
    public class MyCustomAbility : CustomAbility
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomAbility<MyCustomAbility>()
    .WithName(new CustomNameInfo("My Custom Ability"))
    .WithDescription(new CustomNameInfo("My Custom Ability is very cool and does a lot of great stuff"));
    .WithSprite(Properties.Resources.MyCustomAbility)
    .WithUnlock(new AbilityUnlock { UnlockCost = 10, CharacterCreationCost = 5 });
    }
    }
    info

    See Custom Names, Custom Sprites for more info.

    Unlock Properties

    You can use the following properties when initializing AbilityUnlocks:

    PropertyDefaultDescription
    UnlockCost0Unlock cost of the ability, in nuggets. If set to 0, it will unlock automatically, once all prerequisites are unlocked.
    CharacterCreationCost1Cost of the ability in Character Creation, in points.
    IsAvailabletrueDetermines whether the ability is available in the ... Well, there's no menu for custom abilities at the moment, but if there was, this property would determine whether it's available in that menu.
    IsAvailableInCCtrueDetermines whether the ability is available in the Character Creation menu.
    PrerequisitesDetermines what unlocks must be unlocked in order to unlock this ability.
    RecommendationsJust shows these unlocks in a separate Recommendations paragraph in the menus.

    Other properties should not be used during initialization.

    Examples

    ""


    - + \ No newline at end of file diff --git a/docs/dev/items/abilities/rechargeable-abilities/index.html b/docs/dev/items/abilities/rechargeable-abilities/index.html index d5f548e7..6b3090d3 100644 --- a/docs/dev/items/abilities/rechargeable-abilities/index.html +++ b/docs/dev/items/abilities/rechargeable-abilities/index.html @@ -5,14 +5,14 @@ Rechargeable Abilities | RogueLibs Documentation - +

    Rechargeable Abilities

    Custom abilities can be made rechargeable by implementing the IAbilityRechargeable interface. Ability's Count here works as a cooldown and usually represents the amount of time to wait until full recharge. This interface makes use of some of the game's recharging mechanics, but it doesn't completely rely on it. I'd recommend taking a look at Recharging Items, if you need finer control.

    Making abilities rechargeable

    Just implement the IAbilityRechargeable interface in your ability's class:

    MyRechargeableAbility.cs
    public class MyRechargeableAbility : CustomAbility, IAbilityRechargeable
    {
    public void OnRecharging(AbilityRechargingArgs e) { /* ... */ }
    }
    Pro-tip

    You can just set it to 0 when it's fully recharged and to 1 when it's recharging (you can override the displayed count if you want), and use your own cooldown mechanism instead. See Recharging Items for more info.

    OnRecharging works like Unity's Update, but with a settable interval (default is 1 second):

        public void OnRecharging(AbilityRechargingArgs e)
    {
    e.UpdateDelay = 2f; // 1 update every 2 seconds
    Count--;
    }
    info

    You're responsible for decrementing the ability's Count. So, don't forget to do that.
    Ability will stop recharging when Count reaches 0. To start recharging again, just set Count to any other value.

    Examples

    namespace RogueLibsCore.Test
    {
    public class Titan : CustomAbility, IAbilityRechargeable
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomAbility<Titan>()
    .WithName(new CustomNameInfo("Titan"))
    .WithDescription(new CustomNameInfo("Willpower alone isn't enough in battle."))
    .WithSprite(Properties.Resources.Titan)
    .WithUnlock(new AbilityUnlock
    {
    UnlockCost = 10,
    CharacterCreationCost = 10,
    Prerequisites = { VanillaItems.Giantizer },
    });
    }

    public override void OnAdded() { }
    public override void OnPressed()
    {
    if (Count != 0)
    {
    gc.audioHandler.Play(Owner, VanillaAudio.CantDo);
    return;
    }
    Owner!.statusEffects.AddStatusEffect(VanillaEffects.Giant, 15);
    Count = 30;
    }
    public void OnRecharging(AbilityRechargingArgs e)
    {
    e.UpdateDelay = 1f;
    Count--;
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/items/abilities/targetable-abilities/index.html b/docs/dev/items/abilities/targetable-abilities/index.html index 9339b967..021a0262 100644 --- a/docs/dev/items/abilities/targetable-abilities/index.html +++ b/docs/dev/items/abilities/targetable-abilities/index.html @@ -5,13 +5,13 @@ Targetable Abilities | RogueLibs Documentation - +

    Targetable Abilities

    Custom abilities can be made targetable by implementing the IAbilityTargetable interface. You can determine the target of the special ability (for example, the closest agent), and a special ability indicator will be displayed over it. Then you'll be able to access the determined target through the CurrentTarget property.

    Making abilities targetable

    Just implement the IAbilityTargetable interface in your ability's class:

    MyTargetableAbility.cs
    public class MyTargetableAbility : CustomAbility, IAbilityTargetable
    {
    public PlayfieldObject FindTarget() { /* ... */ }
    }

    FindTarget determines the closest (or the most compatible/applicable) target, that the ability can be used on right now. If the ability cannot be used right now, you should return null. Then, you can use CurrentTarget in any of the methods:

        public override void OnPressed()
    {
    if (CurrentTarget is null)
    {
    gc.audioHandler.Play(Owner, "CantDo");
    return;
    }
    /* ... */
    }

    Examples

    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;

    namespace RogueLibsCore.Test
    {
    public class Hug : CustomAbility, IAbilityTargetable
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomAbility<Hug>()
    .WithName(new CustomNameInfo("Hug"))
    .WithDescription(new CustomNameInfo("Sneak up behind people. And HUG THEM!!"))
    .WithSprite(Properties.Resources.Hug)
    .WithUnlock(new AbilityUnlock { UnlockCost = 5, CharacterCreationCost = 5 });

    RogueLibs.CreateCustomName("HugNegative1", NameTypes.Dialogue, new CustomNameInfo("Huh? What are you doing?"));
    RogueLibs.CreateCustomName("HugNegative2", NameTypes.Dialogue, new CustomNameInfo("Excuse me?!"));
    RogueLibs.CreateCustomName("HugNegative3", NameTypes.Dialogue, new CustomNameInfo("Stop it!"));
    RogueLibs.CreateCustomName("HugPositive1", NameTypes.Dialogue, new CustomNameInfo("Oh.. Thanks."));
    RogueLibs.CreateCustomName("HugPositive2", NameTypes.Dialogue, new CustomNameInfo("Um.. Okay.."));
    RogueLibs.CreateCustomName("HugPositive3", NameTypes.Dialogue, new CustomNameInfo("?.."));
    RogueLibs.CreateCustomName("HugForgive1", NameTypes.Dialogue, new CustomNameInfo("Oh.. Okay, I forgive you."));
    RogueLibs.CreateCustomName("HugForgive2", NameTypes.Dialogue, new CustomNameInfo("Alright, I forgive you."));
    RogueLibs.CreateCustomName("HugForgive3", NameTypes.Dialogue, new CustomNameInfo("Okay... Don't worry about that.."));
    }

    public override void OnAdded() { }
    public PlayfieldObject? FindTarget()
    {
    Agent? closest = null;
    float distance = float.MaxValue;
    foreach (Agent agent in Owner!.interactionHelper.TriggerList
    .Where(static go => go.CompareTag("AgentSprite"))
    .Select(static go => go.GetComponent<ObjectSprite>().agent))
    {
    if (!huggedList.Contains(agent) && !agent.dead && !agent.ghost && !Owner.ghost && !agent.hologram
    && agent.go.activeSelf && !agent.mechFilled && !agent.mechEmpty)
    {
    float dist = Vector2.Distance(Owner.curPosition, agent.curPosition);
    if (dist < distance)
    {
    closest = agent;
    distance = dist;
    }
    }
    }
    return closest;
    }
    private readonly List<Agent> huggedList = new List<Agent>();
    public override void OnPressed()
    {
    if (CurrentTarget is null)
    {
    gc.audioHandler.Play(Owner, VanillaAudio.CantDo);
    }
    else
    {
    Agent target = (Agent)CurrentTarget;
    int rnd = new System.Random().Next(3) + 1;

    relStatus code = target.relationships.GetRelCode(Owner);
    if (code is relStatus.Friendly or relStatus.Submissive)
    {
    target.SayDialogue("HugPositive" + rnd);
    target.relationships.SetRel(Owner, "Loyal");
    }
    else if (code == relStatus.Loyal)
    {
    target.SayDialogue("HugPositive" + rnd);
    target.relationships.SetRel(Owner, "Aligned");
    }
    else if (code == relStatus.Aligned)
    {
    target.SayDialogue("HugPositive" + rnd);
    }
    else if (code == relStatus.Neutral)
    {
    target.SayDialogue("HugNegative" + rnd);
    target.relationships.SetRel(Owner, "Annoyed");
    target.relationships.SetStrikes(Owner, 2);
    target.statusEffects.annoyeders.Add(Owner);
    gc.audioHandler.Play(target, VanillaAudio.AgentAnnoyed);
    return;
    }
    else if (code == relStatus.Annoyed)
    {
    target.SayDialogue("HugForgive" + rnd);
    target.relationships.SetRel(Owner, "Neutral");
    }
    else if (code == relStatus.Hostile)
    {
    return;
    }
    target.relationships.SetStrikes(Owner, 0);
    gc.audioHandler.Play(target, VanillaAudio.AgentOK);
    huggedList.Add(target);
    }
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/items/combinable-items/index.html b/docs/dev/items/combinable-items/index.html index fdf0d6ac..5b6eab7a 100644 --- a/docs/dev/items/combinable-items/index.html +++ b/docs/dev/items/combinable-items/index.html @@ -5,13 +5,13 @@ Combinable Items | RogueLibs Documentation - +

    Combinable Items

    Custom items can be made combinable with other items by implementing the IItemCombinable interface. You can define what kind of items your item is combinable with, what happens when you combine these items, and what tooltips to display in the combinable item's cell, and when hovering over it.

    Making items combinable

    Just implement the IItemCombinable interface in your item's class:

    MyCombinableItem.cs
    public class MyCombinableItem : CustomItem, IItemCombinable
    {
    public bool CombineFilter(InvItem other) { /* ... */ }
    public bool CombineItems(InvItem other) { /* ... */ }
    public CustomTooltip CombineTooltip(InvItem other) { /* ... */ }
    public CustomTooltip CombineCursorText(InvItem other) { /* ... */ }
    }

    Plus, your item's type must be "Combine":

        public override void SetupDetails()
    {
    Item.itemType = ItemTypes.Combine;
    /* ... */
    }

    CombineFilter determines what items will be highlighted, when combining the current item.

    CombineItems combines the current item with the other one. The return value indicates whether it was a success or not. Usually you'd just play a "CantDo" sound, if the items cannot be combined. Returning true will also play an animation.

    CombineTooltip determines the tooltip in the upper-left corner of the inventory slot. Text set to null will default to an empty string, and Color set to null will default to

    #FFED00
    . See the tool below.

    CombineCursorText determines the cursor text when hovering over the item. Text set to null will default to "Combine", and Color set to null will default to

    #FFFFFF
    .

    Inventory Slot Preview

    Wanna see how your CombineTooltip will look in the game? Check out this small tool:

    $123

    Examples

    A pretty complicated example with a lot of math.

    using UnityEngine;

    namespace RogueLibsCore.Test
    {
    [ItemCategories(RogueCategories.Technology, RogueCategories.GunAccessory, RogueCategories.Guns)]
    public class AmmoBox : CustomItem, IItemCombinable
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<AmmoBox>()
    .WithName(new CustomNameInfo("Ammo Box"))
    .WithDescription(new CustomNameInfo("Combine with any refillable weapon to refill it. Limited ammo."))
    .WithSprite(Properties.Resources.AmmoBox)
    .WithUnlock(new ItemUnlock
    {
    UnlockCost = 10,
    LoadoutCost = 5,
    CharacterCreationCost = 3,
    Prerequisites = { VanillaItems.KillAmmunizer },
    });
    }

    public override void SetupDetails()
    {
    Item.itemType = ItemTypes.Combine;
    Item.itemValue = 4;
    Item.initCount = 100;
    Item.rewardCount = 200;
    Item.hasCharges = true;
    Item.stackable = true;
    }
    public bool CombineFilter(InvItem other) => other.itemType == ItemTypes.WeaponProjectile && !other.noRefills;
    public bool CombineItems(InvItem other)
    {
    if (!CombineFilter(other))
    {
    gc.audioHandler.Play(Owner, VanillaAudio.CantDo);
    return false;
    }
    if (other.invItemCount >= other.maxAmmo)
    {
    Owner!.SayDialogue("AmmoDispenserFull");
    gc.audioHandler.Play(Owner, VanillaAudio.CantDo);
    return false;
    }

    int amountToRefill = other.maxAmmo - other.invItemCount;
    float singleCost = (float)other.itemValue / other.maxAmmo;
    if (Owner!.oma.superSpecialAbility && Owner.agentName is VanillaAgents.Soldier or VanillaAgents.Doctor)
    singleCost = 0f;

    int affordableAmount = (int)Mathf.Ceil(Count / singleCost);
    int willBeBought = Mathf.Min(affordableAmount, amountToRefill);
    int willBeReduced = (int)Mathf.Min(Count, willBeBought * singleCost);

    Count -= willBeReduced;
    other.invItemCount += willBeBought;
    Owner.SayDialogue("AmmoDispenserFilled");
    gc.audioHandler.Play(Owner, VanillaAudio.BuyItem);
    return true;
    }

    public CustomTooltip CombineTooltip(InvItem other)
    {
    if (!CombineFilter(other)) return default;

    int amountToRefill = other.maxAmmo - other.invItemCount;
    if (amountToRefill == 0) return default;

    float singleCost = (float)other.itemValue / other.maxAmmo;
    if (Owner!.oma.superSpecialAbility && Owner.agentName is VanillaAgents.Soldier or VanillaAgents.Doctor)
    singleCost = 0f;
    int cost = (int)Mathf.Floor(amountToRefill * singleCost);
    int canAfford = (int)Mathf.Ceil(Count / singleCost);

    return "+" + Mathf.Min(amountToRefill, canAfford) + " (" + Mathf.Min(cost, Count) + ")";
    }

    public CustomTooltip CombineCursorText(InvItem other) => gc.nameDB.GetName("RefillGun", NameTypes.Interface);
    // it's one of the vanilla dialogues, so there's no need to define it in the mod
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/items/create-item/index.html b/docs/dev/items/create-item/index.html index a1433cab..3a80d98d 100644 --- a/docs/dev/items/create-item/index.html +++ b/docs/dev/items/create-item/index.html @@ -5,13 +5,13 @@ Creating a Custom Item | RogueLibs Documentation - +

    Creating a Custom Item

    RogueLibs provides classes and methods to create: usable, combinable, targetable (and targetable+) items. All custom items derive from the CustomItem class, which provides all of the basic item functionality. You can derive your custom item's class from specialized interfaces to expand its functionality (IItemUsable, IItemCombinable, IItemTargetable, IItemTargetableAnywhere). Custom items are initialized and integrated into the game using the RogueLibs.CreateCustomItem<TItem>() method.

    CustomItem class

    To make a custom item, you need to create a class deriving from CustomItem:

    MyCustomItem.cs
    public class MyCustomItem : CustomItem
    {
    /* ... */
    }

    There's only one method that you need to implement - SetupDetails:

    MyCustomItem.cs
    public class MyCustomItem : CustomItem
    {
    public override void SetupDetails()
    {
    Item.itemType = ItemTypes.Tool;
    Item.itemValue = 200;
    Item.initCount = 1;
    Item.rewardCount = 1;
    Item.stackable = true;
    Item.hasCharges = true;
    }
    }

    This method is called only once, when the item is created or spawned. See more info later on this page.

    You should add categories using the ItemCategories attribute instead of adding them in SetupDetails:

    MyCustomItem.cs
    [ItemCategories(RogueCategories.Usable, RogueCategories.Weird, "MyCustomCategory")]
    public class MyCustomItem : CustomItem
    {
    /* ... */
    }
    Pro-tip: String consts

    Use static types with string consts, like RogueCategories and ItemTypes. This way you won't make a typo. Typos can be critical sometimes, since neither the game nor RogueLibs track all existing item categories (although it's an interesting idea, maybe I'll do something like that).

    Initialization

    Just call the CreateCustomItem method with your item's type as a parameter:

    MyCustomItem.cs
    public class MyCustomItem : CustomItem
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<MyCustomItem>();
    }
    }
    note

    See more about the RLSetup attribute here.

    You can set your item's name and description using WithName and WithDescription methods:

    MyCustomItem.cs
    public class MyCustomItem : CustomItem
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<MyCustomItem>();
    .WithName(new CustomNameInfo("My Custom Item"))
    .WithDescription(new CustomNameInfo("My Custom Item is very cool and does a lot of great stuff"));
    }
    }

    You can do the same with sprites and unlocks:

    MyCustomItem.cs
    public class MyCustomItem : CustomItem
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<MyCustomItem>();
    .WithName(new CustomNameInfo("My Custom Item"))
    .WithDescription(new CustomNameInfo("My Custom Item is very cool and does a lot of great stuff"));
    .WithSprite(Properties.Resources.MyCustomItem)
    .WithUnlock(new ItemUnlock { UnlockCost = 10, CharacterCreationCost = 5, LoadoutCost = 4, });
    }
    }
    info

    See Custom Names, Custom Sprites for more info.

    Unlock Properties

    You can use the following properties when initializing ItemUnlocks:

    PropertyDefaultDescription
    UnlockCost0Unlock cost of the item, in nuggets. If set to 0, it will unlock automatically, once all prerequisites are unlocked.
    CharacterCreationCost1Cost of the item in Character Creation, in points.
    LoadoutCost1Cost of the item in Loadout, in nuggets.
    IsAvailabletrueDetermines whether the item is available in the Rewards menu.
    IsAvailableInCCtrueDetermines whether the item is available in the Character Creation menu.
    IsAvailableInItemTeleportertrueDetermines whether the item is available in Item Teleporter's menu.
    PrerequisitesDetermines what unlocks must be unlocked in order to unlock this item.
    RecommendationsJust shows these unlocks in a separate Recommendations paragraph in the menus.

    Other properties should not be used during initialization.

    Implementing SetupDetails

    Alright, while the code generator is being worked on, use the following tables:

    Field nameDescription
    itemTypeDetermines how the item will work in the game and stuff.
    initCountDetermines the initial amount of the item.
    rewardCount(optional) Determines the amount of the item that you will get from quests. Defaults to initCount
    itemValueDetermines the cost of a single unit of the item. Costs of weapons are calculated differently - cost of a weapon with 100 durability, or cost of a weapon with its maxAmmo.
    stackableDetermines whether the item is stackable or has charges or something like that. If not set, the item's count is not displayed.
    noCountText(optional) Determines whether the item's count should not be displayed, even if the field above is set to true.
    Field nameDescription
    healthChangeDetermines how much health the item will restore.
    statusEffectDetermines the status effect that the item has. Also means that the item can be used on the Air Conditioner.
    contentsJust like statusEffect, but as a list.
    stackableContents???
    goesInToolbarDetermines whether the item can be set to the toolbar and then be used with 1-5 keys.

    Field nameDescription
    canRepeatInShopDetermines whether there can be two of these items in a shop.
    nonStackableInShopDetermines whether shops should have only 1 item per slot.
    cantBeClonedDetermines whether the item shouldn't be cloneable with the Clone Machine.
    cantStoreInATMMachineDetermines whether players shouldn't be able to store the item in the ATM.
    notInLoadoutMachineDetermines whether the item will not appear in Loadout-O-Matic when selected as a starting item.
    destroyAtLevelEndDetermines whether the item will be destroyed on the next level.
    cantDropDetermines whether the item cannot be dropped.
    doSpillDetermines whether the item should drop from NPCs. Default: true.
    cantDropNPCThe opposite of doSpill. You probably should set these at the same time. Default: false.
    cantDropSpecificCharacterSet to the agent's name, if it shouldn't be droppable by that agent or by custom characters that have it as a starting item.
    characterExclusiveSet this to true, if your item is exclusive to a specific agent and custom characters.
    characterExclusiveSpecificCharacterSet this to the agent's name, if it's exclusive to a specific agent and custom characters.

    - + \ No newline at end of file diff --git a/docs/dev/items/inventory-checks/index.html b/docs/dev/items/inventory-checks/index.html index 08cbaff9..f0a5fd1f 100644 --- a/docs/dev/items/inventory-checks/index.html +++ b/docs/dev/items/inventory-checks/index.html @@ -5,13 +5,13 @@ Inventory Checks | RogueLibs Documentation - +

    Inventory Checks

    Custom usable items might have varying restrictions, and it will be hard to enforce a consistent standard for that kind of stuff. That's why RogueLibs introduces special inventory checks, that will run before and after any usable/combinable/targetable items in the game. You can use these checks to prevent the item's intended function, or maybe somehow augment it.

    InventoryChecks

    With inventory checks, you can omit code like this:

    if (Owner.statusEffects.hasTrait("BloodRestoresHealth"))
    {
    Owner.SayDialogue("WontEatThis");
    return;
    }
    if (Owner.statusEffects.hasTrait("OilRestoresHealth"))
    {
    Owner.SayDialogue("WontEatThat");
    return;
    }
    if (Owner.health == Owner.maxHealth)
    {
    Owner.SayDialogue("NoImFull");
    return;
    }
    ...

    These inventory checks can also implement some common side effects, that modders sometimes forget to implement. For example, removing an item from the inventory or stopping the interaction, when the item's count reaches 0 (RogueLibs provides these checks).

    Ignoring inventory checks

    Inventory checks can be ignored by using an IgnoreChecks attribute.

    You can put IgnoreChecks attributes on your item's class or on any of the interface methods.

    [ItemCategories(RogueCategories.Food, RogueCategories.Weird, "Meat")]
    public class MysteryFood : CustomItem, IItemUsable
    {
    [IgnoreChecks("VegetarianCheck")]
    public bool UseItem()
    {
    e.User.ChangeHealth(Item.healthChange);
    Count--;
    e.User.Say("Huh, tasty. I wonder what that was..."); // O_o
    return true;
    }
    }

    See the table of inventory checks implemented by RogueLibs later on this page.

    Adding inventory checks

    RogueEventArgs class has two properties: Cancel and Handled. If you set Handled to true, then all other checks will be skipped. If you set Cancel to true, then the action that was going to happen will not happen. Usually, they are set to true at the same time.

    For example, there's a trait called "Vegetarian" that should prohibit the player from consuming food with "Meat" category:

    InventoryChecks.AddItemUsingCheck("VegetarianCheck", VegetarianCheck);
    ...
    public static void VegetarianCheck(OnItemUsingArgs e)
    {
    if (e.Item.itemType == ItemTypes.Food && e.User.HasTrait("Vegetarian") && e.Item.Categories.Contains("Meat"))
    {
    // do something to indicate why the item cannot be used
    e.User.gc.audioHandler.Play(e.User, "CantDo");
    e.User.SayDialogue("WontEatMeat");
    // set Cancel and Handled to true
    e.Cancel = e.Handled = true;
    }
    };

    If you want to override an inventory check from another mod, then you'll have to patch it with Harmony.

    Table of default checks

    IItemUsable checks

    NameCriteriaDialogue
    GhostPlayer is a ghost.-
    PeaBrained"Pea-Brained" trait, NOT Food type."GRRRRRRRR!!!!!"
    OnlyOil"Oil Reliant" trait, Food type and (Food or Alcohol category)."I'm gonna need some oil..."
    OnlyOilMedicine"Oil Reliant" trait, Consumable type and Health category."I'm gonna need some oil..."
    OnlyBlood"Jugularious" trait, Food type and (Food or Alcohol category)."Ew gross, I'm not putting that in my mouth!"
    OnlyBloodMedicine"Jugularious" trait, Consumable type and Health category."Modern medicine is for humans, I want BLOOD!"
    OnlyCharge"Electronic" trait, Food type and Food category."I don't exactly have a stomach."
    OnlyHumanFlesh"Strict Cannibal" trait, Food type and Food category."Ew gross, I'm not putting that in my mouth!"
    FullHealthPlayer's health is full and the item's healthChange is greater than 0."No need, I'm feelin' good!"

    These checks are exposed via DefaultInventoryChecks. The rest are implemented inside RogueLibs' patches.

    IItemCombinable checks

    NameCriteriaWhat will happen
    AutoStackingItems have the same nameThey will be highlighted, and once combined, they will be stacked together
    StopOnZeroThe current item's count is 0, or it's no longer in the inventoryInteraction/combining will be stopped

    IItemTargetable checks

    NameCriteriaWhat will happen
    DistanceThe target object is over 15 units awayfalse
    ButlerBotThe target is a Butler Botfalse
    EmptyMechThe target is an Empty Mechfalse
    StopOnZeroThe current item's count is 0, or it's no longer in the inventoryInteraction/targeting will be stopped
    - + \ No newline at end of file diff --git a/docs/dev/items/recharging-items/index.html b/docs/dev/items/recharging-items/index.html index 0b1c014e..ded5b3c6 100644 --- a/docs/dev/items/recharging-items/index.html +++ b/docs/dev/items/recharging-items/index.html @@ -5,13 +5,13 @@ Recharging Items | RogueLibs Documentation - +

    Recharging Items

    RogueLibs doesn't provide any explicit functionality for rechargeable items, but you can easily implement that yourself, using the IDoUpdate interface. You'll find some useful code snippets below, that you can reuse for your own items.

    Making items rechargeable

    Make your custom item's class implement the IDoUpdate interface:

    MyRechargeableItem.cs
    public class MyRechargeableItem : CustomItem, IDoUpdate
    {
    /* ... */
    }

    Presets

    Cooldown represents the amount of seconds to wait until full recharge.

        public float Cooldown { get; private set; }
    public void Update() => Cooldown = Mathf.Max(Cooldown - Time.deltaTime, 0f);

    With adjustable recharging speed:

        public float RechargeSpeed = 1f;

    public float Cooldown { get; private set; }
    public void Update() => Cooldown = Mathf.Max(Cooldown - Time.deltaTime * RechargeSpeed, 0f);

    Usage:

        public bool UseItem()
    {
    if (Cooldown != 0f) return false;
    /* ... */
    Cooldown = 1.5f;
    return true;
    }
    info

    You can use other activation methods too, like CombineItems, TargetObject, TargetPosition and etc.

    If you want to display Cooldown as the item's count, then override the GetCountString method:

        public override CustomTooltip GetCountString()
    {
    if (Cooldown != 0f) return new CustomTooltip(Cooldown, Color.red);
    return base.GetCountString(); // display default count
    }

    note

    There's also a vanilla way of recharging items, but it's really messy and unreliable.

    Examples

    using UnityEngine;

    namespace RogueLibsCore.Test
    {
    [ItemCategories(RogueCategories.Food, RogueCategories.Technology)]
    public class QuantumFud : CustomItem, IItemUsable, IDoUpdate
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<QuantumFud>()
    .WithName(new CustomNameInfo("Quantum Fud"))
    .WithDescription(new CustomNameInfo("A very complicated piece of quantum technology. When you eat it, its quantum equivalent clone is consumed, while the original thing remains intact."))
    .WithSprite(Properties.Resources.QuantumFud)
    .WithUnlock(new ItemUnlock
    {
    UnlockCost = 10,
    LoadoutCost = 15,
    CharacterCreationCost = 10,
    Prerequisites = { VanillaItems.FoodProcessor },
    });
    }

    public override void SetupDetails()
    {
    Item.itemType = ItemTypes.Food;
    Item.itemValue = 180;
    Item.healthChange = 1;
    Item.cantBeCloned = true;
    Item.goesInToolbar = true;
    }

    public float Cooldown { get; set; }
    public void Update() => Cooldown = Mathf.Max(Cooldown - Time.deltaTime, 0f);

    public bool UseItem()
    {
    if (Cooldown != 0f) return false;

    int heal = new ItemFunctions().DetermineHealthChange(Item, Owner);
    Owner!.statusEffects.ChangeHealth(heal);

    if (Owner.HasTrait(VanillaTraits.ShareTheHealth)
    || Owner.HasTrait(VanillaTraits.ShareTheHealth2))
    new ItemFunctions().GiveFollowersHealth(Owner, heal);

    gc.audioHandler.Play(Owner, VanillaAudio.UseFood);
    Cooldown = 0.5f;
    return true;
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/items/targetable-items-plus/index.html b/docs/dev/items/targetable-items-plus/index.html index a5450a27..7a2accd2 100644 --- a/docs/dev/items/targetable-items-plus/index.html +++ b/docs/dev/items/targetable-items-plus/index.html @@ -5,13 +5,13 @@ Targetable Items + | RogueLibs Documentation - +

    Targetable Items +

    Custom items can be made targetable+ (targetable anywhere) by implementing the IItemTargetableAnywhere. Normal targetable items can only be used on something actually present in the game, but as that parenthesised text implies, targetable+ items can be used anywhere on the screen. And so, this interface uses in-game positions instead of objects.

    Making items targetable anywhere

    Just implement the IItemTargetableAnywhere interface in your item's class:

    MyTargetableAnywhereItem.cs
    public class MyTargetableAnywhereItem : CustomItem, IItemTargetableAnywhere
    {
    public bool TargetFilter(Vector2 position) { /* ... */ }
    public bool TargetPosition(Vector2 position) { /* ... */ }
    public CustomTooltip TargetCursorText(Vector2 position) { /* ... */ }
    }

    TargetFilter determines where the cursor should be highlighted, when using the current item.

    TargetPosition uses the current item on the position. The return value indicates whether it was a success or not. You can play a "CantDo" sound and make the player say something, if the item cannot be used. Returning true will also play an animation.

    TargetCursorText determines the text under the cursor when hovering over the specified position. Text set to null will default to "Use", and Color set to null will default to

    #FFFFFF
    .

    Examples

    using UnityEngine;

    namespace RogueLibsCore.Test
    {
    [ItemCategories(RogueCategories.Usable, RogueCategories.Technology, RogueCategories.Stealth)]
    public class UsableTeleporter : CustomItem, IItemTargetableAnywhere
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<UsableTeleporter>()
    .WithName(new CustomNameInfo("Usable Teleporter"))
    .WithDescription(new CustomNameInfo("Teleports you somewhere. Has limited uses."))
    .WithSprite(Properties.Resources.UsableTeleporter)
    .WithUnlock(new ItemUnlock
    {
    UnlockCost = 10,
    LoadoutCost = 9,
    CharacterCreationCost = 5,
    Prerequisites = { VanillaItems.QuickEscapeTeleporter, nameof(WildBypasser) },
    });

    TeleportCursorText = RogueLibs.CreateCustomName("TeleportHere", NameTypes.Interface, new CustomNameInfo("Teleport here"));
    }
    private static CustomName TeleportCursorText = null!;

    public override void SetupDetails()
    {
    Item.itemType = ItemTypes.Tool;
    Item.itemValue = 80;
    Item.initCount = 2;
    Item.rewardCount = 3;
    Item.stackable = true;
    Item.goesInToolbar = true;
    }
    public bool TargetFilter(Vector2 position)
    {
    TileData tileData = gc.tileInfo.GetTileData(position);
    return !gc.tileInfo.IsOverlapping(position, "Anything") && tileData.wallMaterial == wallMaterialType.None;
    }
    public bool TargetPosition(Vector2 position)
    {
    if (!TargetFilter(position)) return false;

    Owner!.SpawnParticleEffect("Spawn", Owner.tr.position);
    Owner.Teleport(position, false, true);
    Owner.rb.velocity = Vector2.zero;
    Owner.SpawnParticleEffect("Spawn", Owner.tr.position, false);
    gc.audioHandler.Play(Owner, VanillaAudio.Spawn);

    Count--;
    return true;
    }
    public CustomTooltip TargetCursorText(Vector2 position) => TeleportCursorText;
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/items/targetable-items/index.html b/docs/dev/items/targetable-items/index.html index 2e0d5aa0..1ed2270d 100644 --- a/docs/dev/items/targetable-items/index.html +++ b/docs/dev/items/targetable-items/index.html @@ -5,13 +5,13 @@ Targetable Items | RogueLibs Documentation - +

    Targetable Items

    Custom items can be made targetable by implementing the IItemTargetable interface. You can define what kind of objects, agents, items on the ground, projectiles or whatever, your item is compatible with, what happens when you use the item on that thing, and what text to display under the mouse cursor when hovering over something.

    Making items targetable

    Just implement the IItemTargetable interface in your item's class:

    MyTargetableItem.cs
    public class MyTargetableItem : CustomItem, IItemTargetable
    {
    public bool TargetFilter(PlayfieldObject target) { /* ... */ }
    public bool TargetObject(PlayfieldObject target) { /* ... */ }
    public CustomTooltip TargetCursorText(PlayfieldObject target) { /* ... */ }
    }

    TargetFilter determines what objects will be highlighted, when using the current item.

    TargetObject uses the current item on the target. The return value indicates whether it was a success or not. You can play a "CantDo" sound and make the player say something, if the item cannot be used. Returning true will also play an animation.

    TargetCursorText determines the text under the cursor when hovering over the specified object. Text set to null will default to "Use", and Color set to null will default to

    #FFFFFF
    .

    Examples

    SPYTRON 3000 from Team Fortress 2. Makes you look like the selected person and copies their relationships.

    namespace RogueLibsCore.Test
    {
    [ItemCategories(RogueCategories.Social, RogueCategories.Stealth,
    RogueCategories.Technology, RogueCategories.Usable)]
    public class SPYTRON3000 : CustomItem, IItemTargetable
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<SPYTRON3000>()
    .WithName(new CustomNameInfo("SPYTRON 3000"))
    .WithDescription(new CustomNameInfo("Always wanted to be someone else? Now you can!"))
    .WithSprite(Properties.Resources.SPYTRON3000)
    .WithUnlock(new ItemUnlock
    {
    UnlockCost = 10,
    LoadoutCost = 2,
    CharacterCreationCost = 3,
    Prerequisites = { VanillaItems.BodySwapper },
    });

    DisguiseCursorText = RogueLibs.CreateCustomName("Disguise", NameTypes.Interface, new CustomNameInfo("Disguise as"));
    }
    private static CustomName DisguiseCursorText = null!;

    public override void SetupDetails()
    {
    Item.itemType = ItemTypes.Tool;
    Item.itemValue = 40;
    Item.initCount = 2;
    Item.rewardCount = 3;
    Item.stackable = true;
    Item.goesInToolbar = true;
    }
    public bool TargetFilter(PlayfieldObject target) => target is Agent a && a != Owner;
    public bool TargetObject(PlayfieldObject targetObj)
    {
    if (!TargetFilter(targetObj)) return false;
    Agent target = (Agent)targetObj;

    string prev = Owner!.agentName;
    Owner.agentName = target.agentName;

    Owner.relationships.CopyLooks(target);
    foreach (Relationship rel in target.relationships.RelList)
    {
    Relationship otherRel = rel.agent.relationships.GetRelationship(target);

    Owner.relationships.SetRel(rel.agent, rel.relType);
    Owner.relationships.SetRelHate(rel.agent, 0);
    Owner.relationships.GetRelationship(rel.agent).secretHate = rel.secretHate;
    Owner.relationships.GetRelationship(rel.agent).mechHate = rel.mechHate;

    rel.agent.relationships.SetRel(Owner, otherRel.relType);
    rel.agent.relationships.SetRelHate(Owner, 0);
    rel.agent.relationships.GetRelationship(Owner).secretHate = otherRel.secretHate;
    rel.agent.relationships.GetRelationship(Owner).mechHate = otherRel.mechHate;
    }
    target.relationships.SetRel(Owner, "Hateful");
    target.relationships.SetRelHate(Owner, 25);

    Owner.agentName = prev;

    Owner.gc.audioHandler.Play(Owner, VanillaAudio.Spawn);
    Owner.gc.spawnerMain.SpawnParticleEffect("Spawn", Owner.tr.position, 0f);

    Count--;
    Item.invInterface.HideTarget();
    return true;
    }
    public CustomTooltip TargetCursorText(PlayfieldObject? _) => DisguiseCursorText;
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/items/usable-items/index.html b/docs/dev/items/usable-items/index.html index 248bcebb..c7a51cc1 100644 --- a/docs/dev/items/usable-items/index.html +++ b/docs/dev/items/usable-items/index.html @@ -5,13 +5,13 @@ Usable Items | RogueLibs Documentation - +

    Usable Items

    Custom items can be made usable by implementing the IItemUsable interface, that defines a single method, UseItem. Usable items can be used by right-clicking them in the inventory, or using them from the toolbar (1-5 keys).

    Making items usable

    Just implement the IItemUsable interface in your item's class:

    MyUsableItem.cs
    public class MyUsableItem : CustomItem, IItemUsable
    {
    public bool UseItem() { /* ... */ }
    }

    UseItem's return value indicates whether the item was successfully used. Returning true will also play an animation. When returning false, you can play a "CantDo" sound, and optionally make the current owner say why the item cannot be used:

            if (cantUse)
    {
    gc.audioHandler.Play(Owner, "CantDo");
    Owner.SayDialogue("CantUseItemBecause...");
    // don't forget to create a dialogue with that id
    return false;
    }
    info

    You're responsible for decrementing the item's Count. So, don't forget to do that.

    Examples

    A simple usable item that allows the player to use the Joke ability.

    namespace RogueLibsCore.Test
    {
    [ItemCategories(RogueCategories.Usable, RogueCategories.Social)]
    public class JokeBook : CustomItem, IItemUsable
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<JokeBook>()
    .WithName(new CustomNameInfo("Joke Book"))
    .WithDescription(new CustomNameInfo("Always wanted to be a Comedian? Now you can! (kind of)"))
    .WithSprite(Properties.Resources.JokeBook)
    .WithUnlock(new ItemUnlock
    {
    UnlockCost = 10,
    LoadoutCost = 5,
    CharacterCreationCost = 3,
    Prerequisites = { VanillaAgents.Comedian + "_BQ" },
    });
    }

    public override void SetupDetails()
    {
    Item.itemType = ItemTypes.Tool;
    Item.itemValue = 15;
    Item.initCount = 10;
    Item.rewardCount = 10;
    Item.stackable = true;
    Item.hasCharges = true;
    Item.goesInToolbar = true;
    }
    public bool UseItem()
    {
    if (Owner!.statusEffects.makingJoke) return false;

    string prev = Owner.specialAbility;
    Owner.specialAbility = VanillaAbilities.Joke;
    Owner.statusEffects.PressedSpecialAbility();
    Owner.specialAbility = prev;

    Count--;
    return true;
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/items/weapons/custom-projectiles/index.html b/docs/dev/items/weapons/custom-projectiles/index.html index 67a25e38..126afc77 100644 --- a/docs/dev/items/weapons/custom-projectiles/index.html +++ b/docs/dev/items/weapons/custom-projectiles/index.html @@ -5,13 +5,13 @@ Custom Projectiles* | RogueLibs Documentation - + - + \ No newline at end of file diff --git a/docs/dev/items/weapons/melee-weapons/index.html b/docs/dev/items/weapons/melee-weapons/index.html index ca448c4a..8db88920 100644 --- a/docs/dev/items/weapons/melee-weapons/index.html +++ b/docs/dev/items/weapons/melee-weapons/index.html @@ -5,13 +5,13 @@ Melee Weapons* | RogueLibs Documentation - +

    Melee Weapons*

    Melee weapons are realy complicated. But, thankfully, RogueLibs takes care of the most difficult part — patching. So, all you have to do is to create a class deriving from CustomWeaponMelee. I've spent hundreds of hours patching the melee weapons code, and managed to condense all of it into a single class. It's ridiculous how overcomplicated and wet Matt's code is.

    Work-In-Progress

    Vanilla Weapons Reference

    The vanilla weapon stats can be summarized in the table below:

    TypeCDSpeedAnimation(offset), (size)*Knock forceSound
    Fist-based15Melee-Punch(0, 0), (0.16, 0.16)20 (40 fist)SwingWeaponFist
    1-handed stab15Melee-Knife(0, -0.02), (0.16, 0.36)40SwingWeaponSmall
    1-handed swing15Melee-SwingShort(0, -0.02), (0.16, 0.36)80SwingWeaponLarge
    2-handed swing14Melee-SwingShort(0, -0.02), (0.16, 0.36)80SwingWeaponLarge
    Duster (1-Swing)02Melee-Dust(0, 0), (0, 0)0ButlerBotClean

    * The hitboxes may vary with a weapon, see below.

    TypeWeapon(offset), (size)
    Fist-basedFist(0, 0), (0.16, 0.16)
    Fist-basedChloroform Hankie(0, 0), (0.16, 0.16)
    Fist-basedSticky Glove(0, 0), (0.16, 0.16)
    1-handed stabKnife(0, -0.02), (0.16, 0.36)
    1-handed swingPoliceBaton(0, -0.02), (0.16, 0.36)
    1-handed swingWrench(0, -0.04), (0.16, 0.32)
    2-handed swingSledgehammer(0, -0.02), (0.16, 0.36)
    2-handed swingCrowbar(0, -0.02), (0.16, 0.36)
    2-handed swingPlasmaSword(0, -0.02), (0.16, 0.36)
    2-handed swingAxe(0, -0.02), (0.16, 0.36)
    2-handed swingBaseball Bat(0, -0.03), (0.16, 0.36)
    2-handed swingSword(0, -0.03), (0.16, 0.36)
    - + \ No newline at end of file diff --git a/docs/dev/items/weapons/projectile-weapons/index.html b/docs/dev/items/weapons/projectile-weapons/index.html index 75ad2465..80c73fe8 100644 --- a/docs/dev/items/weapons/projectile-weapons/index.html +++ b/docs/dev/items/weapons/projectile-weapons/index.html @@ -5,13 +5,13 @@ Projectile Weapons* | RogueLibs Documentation - + - + \ No newline at end of file diff --git a/docs/dev/items/weapons/thrown-weapons/index.html b/docs/dev/items/weapons/thrown-weapons/index.html index b612b47a..186307cd 100644 --- a/docs/dev/items/weapons/thrown-weapons/index.html +++ b/docs/dev/items/weapons/thrown-weapons/index.html @@ -5,13 +5,13 @@ Thrown Weapons* | RogueLibs Documentation - + - + \ No newline at end of file diff --git a/docs/dev/names/custom-languages/index.html b/docs/dev/names/custom-languages/index.html index 06bfccea..9fec8656 100644 --- a/docs/dev/names/custom-languages/index.html +++ b/docs/dev/names/custom-languages/index.html @@ -5,13 +5,13 @@ Custom Languages* | RogueLibs Documentation - +

    Custom Languages*

    You can add your custom languages to the game using the LanguageService.RegisterLanguageCode method.

    LanguageService.RegisterLanguageCode("japanese", (LanguageCode)123);
    Work-In-Progress

    There hasn't been much progress on actually implementing custom languages. No one has ever translated the entire game to a different language, not yet included with SoR. When that time comes, feel free to create an issue on GitHub.

    LanguageService

    LanguageService is a static class, that contains all of the things related to localization.

    Work-In-Progress

    Adding localizations for mods

    Work-In-Progress

    Eh. Just ask the creator(s) of the mod, that you want to translate, to include your localization.

    - + \ No newline at end of file diff --git a/docs/dev/names/custom-names/index.html b/docs/dev/names/custom-names/index.html index e6ae4fda..8f49608b 100644 --- a/docs/dev/names/custom-names/index.html +++ b/docs/dev/names/custom-names/index.html @@ -5,13 +5,13 @@ Custom Names | RogueLibs Documentation - +

    Custom Names

    Custom localization in RogueLibs is implemented using instances of the CustomName class, which contain all languages' translations at the same time (which isn't really efficient, but whatever). You can integrate your custom names into the game using the RogueLibs.CreateCustomName(...) method.

    CustomNameInfo structure

    CustomNameInfo structure is used to create custom names and transfer localization data.

    CustomNameInfo emptyInfo = new CustomNameInfo();
    CustomNameInfo nameInfo = new CustomNameInfo("english text");

    You can add more translations to the custom names too:

    nameInfo = new CustomNameInfo
    {
    [LanguageCode.French] = "texte français",
    [LanguageCode.Spanish] = "texto en español",
    };
    // or
    nameInfo[LanguageCode.French] = "texte français";
    nameInfo[LanguageCode.Spanish] = "texto en español";

    You can also use your own language codes:

    nameInfo[(LanguageCode)123] = "日本語テキスト";
    info

    See more info in Custom Languages.


    Unlike dictionaries, both CustomName and CustomNameInfo return null, if they don't contain the specified LanguageCode:

    string translation = nameInfo[(LanguageCode)123];
    // returns null, if that language is not specified
    string display = translation ?? nameInfo.English;

    CustomName class

    Usually, CustomNames are created automatically, when you add names and descriptions to your items, traits, abilities and etc.:

    RogueLibs.CreateCustomItem<MyCustomItem>()
    .WithName(new CustomNameInfo("English name")
    {
    French = "nom français",
    Spanish = "nombre español",
    })
    .WithDescription(new CustomNameInfo("English description")
    {
    French = "description française",
    Spanish = "descripción en español",
    });

    You can initialize them yourself too, although you have to provide the name and type of the CustomName yourself:

    CustomName name = RogueLibs.CreateCustomName("Name", "Type", new CustomNameInfo("Info"));

    If you're going to use the second method, here's the list of types used in the game:

    • Item - item and special ability names;
    • Description - item, special ability, trait, status effect and agent descriptions;
    • StatusEffect - trait and status effect names;
    • Interface - interface buttons, labels and stuff;
    • Unlock - mutator and Big Quest names and descriptions;
    • Object - object and chunk type names;
    • Agent - agent names;
    • Dialogue - agent dialogue lines;
    Pro-tip: Name type const strings

    Use string consts in the NameTypes static class to avoid typos.

    Usage

    If you want to use your custom names in the game, use NameDB.GetName() or any other methods that use it:

    string dialogue = gc.nameDB.GetName("CryForHelp", NameTypes.Dialogue);
    Owner.SayDialogue("CryForHelp");

    CustomNames and CustomNameInfos also can be implicitly converted into CustomTooltip:

    public class Recycler : CustomItem, IItemCombinable
    {
    [RLSetup]
    public static void Setup()
    {
    recycleTooltip = RogueLibs.CreateCustomName("Recycle", NameTypes.Interface, new CustomNameInfo
    {
    English = "Recycle",
    Russian = "Переработать",
    });
    }
    private static CustomName recycleTooltip;

    /* ... */
    public CustomTooltip CombineTooltip(InvItem _) => recycleTooltip;
    }
    - + \ No newline at end of file diff --git a/docs/dev/names/name-providers/index.html b/docs/dev/names/name-providers/index.html index b066b21e..ef122f52 100644 --- a/docs/dev/names/name-providers/index.html +++ b/docs/dev/names/name-providers/index.html @@ -5,13 +5,13 @@ Custom Name Providers | RogueLibs Documentation - +

    Custom Name Providers

    If you have some kind of a complicated localization logic, then you might want to create your own INameProvider. This way you can control what strings are returned by NameDB.GetName in a more generic way. You can even hook up your localization provider, if you don't like the localization system provided by RogueLibs.

    INameProvider interface

    Just create a class implementing INameProvider and add it to RogueFramework:

    MyNameProvider.cs
    public class MyNameProvider : INameProvider
    {
    public void GetName(string name, string type, ref string? result)
    {
    if (name.StartsWith("fake_"))
    {
    string sub = name.Substring("fake_".Length);
    result = LanguageService.NameDB.GetName(sub, type);
    }
    }
    }
    info

    If the original NameDB.GetName returned an error string (with E_ prefix), result is set to null.

    RogueFramework.NameProviders.Add(new MyNameProvider());

    Here's a more practical and useful example, that is already implemented in RogueLibs:

    public class DialogueNameProvider : INameProvider
    {
    public void GetName(string name, string type, ref string? result)
    {
    if (result is null && type == "Dialogue" && name.StartsWith("NA_"))
    {
    string sub = name.Substring("NA_".Length);
    string newResult = LanguageService.NameDB.GetName(sub, type);
    if (!newResult.StartsWith("E_")) result = newResult;
    }
    }
    }

    Normally, the game looks for dialogue names of the following format: <AgentName>_<DialogueName>. If such a name doesn't exist, then NA_<DialogueName> (NA - No Agent) is used instead. This name provider will also look for a name with just the dialogue name. This allows the developers to write dialogue names without that annoying and often confusing NA_ prefix.

    - + \ No newline at end of file diff --git a/docs/dev/patching-utilities/index.html b/docs/dev/patching-utilities/index.html index a05a2df8..7cb0e645 100644 --- a/docs/dev/patching-utilities/index.html +++ b/docs/dev/patching-utilities/index.html @@ -5,13 +5,13 @@ Patching Utilities | RogueLibs Documentation - +

    Patching Utilities

    RogueLibs provides several utilities to help you with patching. Whether you use Harmony's attributes, Harmony instances directly, RogueLibs' stuff or something else, is your choice. All of them have their own pros and cons. You can learn more about Harmony here.

    RLSetup attribute

    Since RogueLibs handles custom stuff as classes, you might forget to initialize a new class in your plugin's Awake. That's why RLSetup attribute is here. You can just add it to a static method, and initialize your custom thing in there.

    MyCustomItem.cs
    public class MyCustomItem : CustomItem
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<MyCustomItem>()
    .WithName(new CustomNameInfo("Name"))
    .WithDescription(new CustomNameInfo("Description"))
    .WithSprite(Properties.Resources.Sprite)
    .WithUnlock(new ItemUnlock());

    RogueLibs.CreateCustomName("SomeName", "Dialogue", new CustomNameInfo("Text"));
    }
    }

    You'll just have to call the following method in your plugin's Awake:

    MyCoolPlugin.cs
        public void Awake()
    {
    RogueLibs.LoadFromAssembly();
    /* ... */
    }
    Pro-tip

    Seriously, you should use it. It helps with versioning too. All of the logic in one place.

    RoguePatcher

    RoguePatcher is a small helper class that makes writing patches a little bit faster and easier. If you need more control (patch order, priority, etc.), then you should use the original Harmony methods.

    RoguePatcher patcher = new RoguePatcher(this);

    patcher.Postfix(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));

    patcher.Postfix(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });

    Pro-tip

    Instead of specifying method names using strings, you should specify them using the nameof keyword. Use string names only if the method you're trying to patch is not public.

    Patch methods should have the following name: <TargetType>_<TargetMethod>. In the example above, RoguePatcher will search for patch methods called StatusEffects_hasStatusEffect and InvDatabase_ChooseArmor in your plugin's class.

    You can change the type to search patch methods in. Specify it in the constructor or set the property between patches:

    public class MyCoolPlugin : BaseUnityPlugin
    {
    public void Awake()
    {
    RoguePatcher patcher = new RoguePatcher(this, typeof(MyCoolPatches));

    patcher.Postfix(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));

    patcher.TypeWithPatches = typeof(MyEvenCoolerPatches);

    patcher.Postfix(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
    }
    }
    public class MyCoolPatches
    {
    public static void StatusEffects_hasStatusEffect(StatusEffects __instance)
    {
    /* ... */
    }
    }
    public class MyEvenCoolerPatches
    {
    public static void InvDatabase_ChooseArmor(InvDatabase __instance, string previousArmorName)
    {
    /* ... */
    }
    }

    Transpiler helper methods

    Transpilers are kind of complicated, since they use a low-level Intermediate Language (IL) instead of C# (branches, loops and conditions are the hardest part in here). As an example, here's one of the simplest transpilers from RogueLibs:

    public static IEnumerable<CodeInstruction> StatusEffects_AddStatusEffect(IEnumerable<CodeInstruction> codeEnumerable)
    => codeEnumerable.AddRegionAfter(
    new Func<CodeInstruction, bool>[]
    {
    i => i.IsLdloc(),
    i => i.opcode == OpCodes.Ldarg_3,
    i => i.opcode == OpCodes.Stfld && i.StoresField(causingAgentField),
    },
    new Func<CodeInstruction[], CodeInstruction>[]
    {
    a => a[0],
    _ => new CodeInstruction(OpCodes.Ldarg_0),
    _ => new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(SetupEffectHook))),
    });

    private static readonly FieldInfo causingAgentField = typeof(StatusEffect).GetField(nameof(StatusEffect.causingAgent));

    Avoid heavy calculations

    When writing predicates, keep in mind that they might get called hundreds or thousands of times. For example, you can pre-calculate the FieldInfo value, used by your predicate, just put it in a static readonly field, like in the example above.

    Heavy calculations like that can cost you hundreds of milliseconds of start-up time (or even entire seconds, if you're working on a big project).

    Here's another example from RogueLibs:

    public static IEnumerable<CodeInstruction> Unlocks_LoadInitialUnlocks(IEnumerable<CodeInstruction> codeEnumerable)
    => codeEnumerable.ReplaceRegion(
    new Func<CodeInstruction, bool>[]
    {
    i => i.opcode == OpCodes.Callvirt && i.Calls(List_Unlock_GetEnumerator),
    i => i.IsStloc(),
    },
    new Func<CodeInstruction, bool>[]
    {
    i => i.opcode == OpCodes.Callvirt,
    i => i.opcode == OpCodes.Endfinally,
    i => i.opcode == OpCodes.Ldarg_0,
    },
    new CodeInstruction[]
    {
    new CodeInstruction(OpCodes.Pop),
    new CodeInstruction(OpCodes.Pop),
    new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(LoadUnlockWrappersAndCategorize))),
    });

    private static readonly MethodInfo List_Unlock_GetEnumerator = typeof(List<Unlock>).GetMethod("GetEnumerator");

    BTHarmonyUtils

    You might also want to consider using BlazingTwist's BTHarmonyUtils. It provides several useful transpiler-patching utilities, similar to the ones in RogueLibs. It is easier to use, but I don't think there's any documentation on it.

    - + \ No newline at end of file diff --git a/docs/dev/traits/create-effect/index.html b/docs/dev/traits/create-effect/index.html index 55be7608..a379cc14 100644 --- a/docs/dev/traits/create-effect/index.html +++ b/docs/dev/traits/create-effect/index.html @@ -5,13 +5,13 @@ Creating a Custom Effect | RogueLibs Documentation - +

    Creating a Custom Effect

    RogueLibs provides classes and methods to create custom effects, and an interface to make status effects updateable. Just like items and traits, custom effects derive from a hook class, CustomEffect. If you want the effect to have some kind of a passive effect, then you might need to patch that in yourself.

    CustomEffect class

    To make a custom effect, you need to create a class deriving from CustomEffect:

    MyCustomEffect.cs
    public class MyCustomEffect : CustomEffect
    {
    /* ... */
    }

    There are 5 methods that you need to implement:

    MyCustomEffect.cs
    public class MyCustomEffect : CustomEffect
    {
    public override int GetEffectTime() { /* ... */ }
    public override int GetEffectHate() { /* ... */ }
    public override void OnAdded() { /* ... */ }
    public override void OnRemoved() { /* ... */ }
    public override void OnUpdated(EffectUpdatedArgs e) { /* ... */ }
    }

    GetEffectTime determines the default status effect time. Traits like "Longer Status Effects", "Longer Status Effects +" and "Shorter Status Effects" are applied after calling this method.

    GetEffectHate determines how much hate other characters will get towards the character who inflicted the status effect on them. Usually, it's 5 for negative effects and 0 for positive effects.

    caution

    GetEffectTime and GetEffectHate are called on partially initialized hooks, so the effect's owner might not actually have the effect. Do not initialize any effect-specific variables in these methods.

    OnAdded is called when the effect is added to a character, and OnRemoved is called when it's removed from a character.

    OnUpdated works like Unity's Update, but with a settable interval (default is 1 second):

        public override void OnUpdated(EffectUpdatedArgs e)
    {
    e.UpdateDelay = 0.5f; // 2 updates per second
    /* ... */
    CurrentTime--;
    }
    info

    You're responsible for decrementing the effect's CurrentTime. So, don't forget to do that.

    All custom effect classes should have an EffectParameters attribute. You can specify whether your effect should be removed on death, on knockout or between levels. Default is RemoveOnDeath.

    [EffectParameters(EffectLimitations.RemoveOnDeath | EffectLimitations.RemoveOnKnockOut)]
    public class MyCustomEffect : CustomEffect
    {
    /* ... */
    }

    Initialization

    Just call the CreateCustomEffect method with your effect's type as a parameter:

    MyCustomEffect.cs
    public class MyCustomEffect : CustomEffect
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomEffect<MyCustomEffect>();
    }
    }
    note

    See more about the RLSetup attribute here.

    You can set your effect's name and description using WithName and WithDescription methods:

    MyCustomEffect.cs
    public class MyCustomEffect : CustomEffect
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomEffect<MyCustomEffect>()
    .WithName(new CustomNameInfo("My Custom Effect"))
    .WithDescription(new CustomNameInfo("My Custom Effect is very cool and does a lot of great stuff"));
    }
    }
    info

    See Custom Names for more info.

    Examples

    A simple effect that just gives a temporary boost to some stats. You can see Adrenaline Shot's (item that gives this effect) implementation in Usable Items: Examples.

    namespace RogueLibsCore.Test
    {
    [EffectParameters(EffectLimitations.RemoveOnDeath | EffectLimitations.RemoveOnKnockOut)]
    public class Adrenaline : CustomEffect
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomEffect<Adrenaline>()
    .WithName(new CustomNameInfo("Adrenaline"))
    .WithDescription(new CustomNameInfo("Gives you a ton of boosts for a short period of time."));
    }

    public override int GetEffectTime() => 15;
    public override int GetEffectHate() => 0;
    public override void OnAdded()
    {
    Owner.ChangeHealth(20);
    Owner.SetStrength(Owner.strengthStatMod + 2);
    Owner.SetEndurance(Owner.enduranceStatMod + 2);
    Owner.SetAccuracy(Owner.accuracyStatMod - 1);
    Owner.SetSpeed(Owner.speedStatMod + 2);
    Owner.critChance += 30;
    }
    public override void OnRemoved()
    {
    Owner.SetStrength(Owner.strengthStatMod - 2);
    Owner.SetEndurance(Owner.enduranceStatMod - 2);
    Owner.SetAccuracy(Owner.accuracyStatMod + 1);
    Owner.SetSpeed(Owner.speedStatMod - 2);
    Owner.critChance -= 30;
    }
    public override void OnUpdated(EffectUpdatedArgs e)
    {
    e.UpdateDelay = 1f;
    CurrentTime--;
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/traits/create-trait/index.html b/docs/dev/traits/create-trait/index.html index 7da8c814..e2a5e11d 100644 --- a/docs/dev/traits/create-trait/index.html +++ b/docs/dev/traits/create-trait/index.html @@ -5,13 +5,13 @@ Creating a Custom Trait | RogueLibs Documentation - +

    Creating a Custom Trait

    RogueLibs provides classes and methods to create custom traits, and an interface to make traits updateable. Just like items, custom traits derive from a hook class, CustomTrait, and their functionality can be expanded with interfaces: ITraitUpdateable, just one in this case. If you want the trait to have some kind of a passive effect, then you might need to patch that in yourself.

    CustomTrait class

    To make a custom trait, you need to create a class deriving from CustomTrait:

    MyCustomTrait.cs
    public class MyCustomTrait : CustomTrait
    {
    /* ... */
    }

    There are 2 methods that you need to implement:

    MyCustomTrait.cs
    public class MyCustomTrait : CustomTrait
    {
    public override void OnAdded() { /* ... */ }
    public override void OnRemoved() { /* ... */ }
    }

    OnAdded is called when the trait is added to a character, and OnRemoved is called when it's removed from a character.

    Updating

    You can make your trait updateable by implementing the ITraitUpdateable interface:

    MyCustomTrait.cs
    public class MyCustomTrait : CustomTrait, ITraitUpdateable
    {
    public void OnUpdated(TraitUpdatedArgs e) { /* ... */ }
    }

    OnUpdated works like Unity's Update, but with a settable interval (default is 1 second):

        public void OnUpdated(TraitUpdatedArgs e)
    {
    e.UpdateDelay = 0.5f; // 2 updates per second
    /* ... */
    }

    Initialization

    Just call the CreateCustomTrait method with your trait's type as a parameter:

    MyCustomTrait.cs
    public class MyCustomTrait : CustomTrait
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomTrait<MyCustomTrait>();
    }
    }
    note

    See more about the RLSetup attribute here.

    You can set your trait's name and description using WithName and WithDescription methods:

    MyCustomTrait.cs
    public class MyCustomTrait : CustomTrait
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomTrait<MyCustomTrait>()
    .WithName(new CustomNameInfo("My Custom Trait"))
    .WithDescription(new CustomNameInfo("My Custom Trait is very cool and does a lot of great stuff"));
    }
    }

    You can do the same with sprites and unlocks:

    MyCustomTrait.cs
    public class MyCustomTrait : CustomTrait
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomTrait<MyCustomTrait>()
    .WithName(new CustomNameInfo("My Custom Trait"))
    .WithDescription(new CustomNameInfo("My Custom Trait is very cool and does a lot of great stuff"))
    // the sprite will be displayed only in the menus (optional)
    .WithSprite(Properties.Resources.MyCustomTrait)
    .WithUnlock(new TraitUnlock { UnlockCost = 10, CharacterCreationCost = 5 });
    }
    }
    info

    See Custom Names, Custom Sprites for more info.

    Unlock Properties

    You can use the following properties when initializing TraitUnlocks:

    PropertyDefaultDescription
    UnlockCost0Unlock cost of the trait, in nuggets. If set to 0, it will unlock automatically, once all prerequisites are unlocked.
    CharacterCreationCost1Cost of the trait in Character Creation, in points.
    IsAvailabletrueDetermines whether the trait is available in the Traits menu and on level-ups. If the trait is negative, set it to false.
    IsAvailableInCCtrueDetermines whether the trait is available in the Character Creation menu.
    CancellationsDetermines what traits cannot co-exist with this trait.
    PrerequisitesDetermines what unlocks must be unlocked in order to unlock this trait.
    RecommendationsJust shows these unlocks in a separate Recommendations paragraph in the menus.

    Other properties should not be used during initialization.

    Examples

    using System;

    namespace RogueLibsCore.Test
    {
    public class Smoker : CustomTrait, ITraitUpdateable
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomTrait<Smoker>()
    .WithName(new CustomNameInfo("Smoker"))
    .WithDescription(new CustomNameInfo("Randomly cough, alerting enemies"))
    .WithUnlock(new TraitUnlock { CharacterCreationCost = -4 });

    RogueLibs.CreateCustomName("Smoker_Cough1", NameTypes.Dialogue, new CustomNameInfo("*Cough*"));
    RogueLibs.CreateCustomName("Smoker_Cough2", NameTypes.Dialogue, new CustomNameInfo("*Cough* *CouGH*"));
    RogueLibs.CreateCustomName("Smoker_Cough3", NameTypes.Dialogue, new CustomNameInfo("*coUGH* *COUgh*"));
    }

    public override void OnAdded() { }
    public override void OnRemoved() { }
    public void OnUpdated(TraitUpdatedArgs e)
    {
    e.UpdateDelay = 5f;

    int rnd = new Random().Next(0, 5);
    if (rnd == 0)
    {
    rnd = new Random().Next(3) + 1;
    Owner.SayDialogue($"Smoker_Cough{rnd}");
    gc.audioHandler.Play(Owner, VanillaAudio.AgentAnnoyed);

    Noise noise = gc.spawnerMain.SpawnNoise(Owner.tr.position, 1f, Owner, "Attract", Owner);
    noise.distraction = true;
    }
    }
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/unlocks/configuring-unlocks/index.html b/docs/dev/unlocks/configuring-unlocks/index.html index 399cf2f9..05866fd3 100644 --- a/docs/dev/unlocks/configuring-unlocks/index.html +++ b/docs/dev/unlocks/configuring-unlocks/index.html @@ -5,13 +5,13 @@ Configuring Unlocks | RogueLibs Documentation - +

    Configuring Unlocks

    Custom unlocks' displayed names, descriptions, images, buttons and their order in the list can be configured. You can even determine your own unlock conditions, change the displayed text and sprites conditionally and stuff like that.

    Sorting

    Unlocks are first sorted by their SortingOrder, then by their state (unlocked, purchasable, available and locked), and then by their SortingIndex. You can ignore the sorting by the state by setting IgnoreStateSorting to true.

    Here's an example of how this sorting would work:

    • SortingOrder = -400:
      • Unlocked:
        • SortingIndex = -3;
        • SortingIndex = 1;
        • SortingIndex = 2;
      • Purchasable:
        • ...
      • Available:
        • ...
      • Locked:
        • ...
    • SortingOrder = -3:
      • ...
    • SortingOrder = 0 (vanilla unlocks go here, with sorting index of 0):
      • ...
    • SortingOrder = 1:
      • ...
    • SortingOrder = 500:
      • ...
    caution

    The menu might get weird or even crash if not all unlocks on the current SortingOrder have IgnoreStateSorting set to the same value. So make sure that all other unlocks have IgnoreStateSorting set to true too.

    Overrideable methods

    UnlockWrapper

    UnlockWrapper.cs
    // called when the unlock is initialized and integrated into the game
    public virtual void SetupUnlock() { }

    // called pretty often to determine if it can be unlocked right now
    public virtual void UpdateUnlock()
    {
    if ((Unlock.nowAvailable = !Unlock.unlocked && CanBeUnlocked()) && UnlockCost is 0)
    gc.unlocks.DoUnlockForced(Name, Type);
    }

    // determines whether the unlock can be unlocked right now
    public virtual bool CanBeUnlocked() => UnlockCost > -1
    && Unlock.prerequisites.TrueForAll(c => gc.sessionDataBig.unlocks.Exists(u => u.unlockName == c && u.unlocked));

    // gets the unlock's raw name, without any rich text, costs and values
    public virtual string GetName() => gc.nameDB.GetName(Name, Unlock.unlockNameType);

    // gets the unlock's raw description, without any rich text, costs and values
    public virtual string GetDescription() => gc.nameDB.GetName(Name, Unlock.unlockDescriptionType);

    // gets the unlock's image (displayed in the menus)
    public virtual Sprite GetImage() => RogueFramework.ExtraSprites.TryGetValue(Name, out Sprite image) ? image;
    Pro-tip

    You can see for yourself how these methods are implemented in RogueLibs' source code.

    DisplayedUnlock

    DisplayedUnlock.cs
    // called when the button is updated. `UpdateUnlock` is called right before this.
    public virtual void UpdateButton() => UpdateButton(IsEnabled, UnlockButtonState.Selected, UnlockButtonState.Normal);

    protected void UpdateButton(bool isEnabledOrSelected, UnlockButtonState selected, UnlockButtonState normal)
    {
    Text = GetFancyName();
    State = IsUnlocked ? isEnabledOrSelected ? selected : normal
    : Unlock.nowAvailable && UnlockCost > -1 ? UnlockButtonState.Purchasable
    : UnlockButtonState.Locked;
    }

    // called when the button is pressed. See other unlocks' implementations for more info
    public abstract void OnPushedButton();

    // gets the unlock's "fancy" name, with rich text formatting, costs and point values
    public virtual string GetFancyName()
    {
    /* A lot of stuff, see RogueLibs' source code for more information */
    }

    // gets the unlock's "fancy" description, with rich text formatting, cancellations, prerequisites and recommendations
    public virtual string GetFancyDescription()
    {
    /* A lot of stuff, see RogueLibs' source code for more information */
    }
    Pro-tip

    You can see for yourself how these methods are implemented in RogueLibs' source code.

    Examples

    Let's say, you want to make an item called Present, and it has 3 different sprites.

    First of all, you'll need to create an unlock class deriving from ItemUnlock:

    PresentUnlock.cs
    public class PresentUnlock : ItemUnlock
    {
    }

    Now you can override the DisplayedUnlock's GetImage method:

    PresentUnlock.cs
    public class PresentUnlock : ItemUnlock
    {
    public override Sprite GetImage()
    {
    int rnd = new System.Random().Next(3) + 1;
    return gc.gameResources.itemDic[$"Present{rnd}"];
    }
    }

    Then just use your custom unlock in the custom item's initialization:

    Present.cs
    public class Present : CustomItem, IItemUsable
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<Present>()
    .WithName(new CustomNameInfo("Present"))
    .WithDescription(new CustomNameInfo("Open it!"))
    .WithSprite(Properties.Resources.Present)
    .WithUnlock(new PresentUnlock
    {
    UnlockCost = 5,
    CharacterCreationCost = 3,
    LoadoutCost = 3
    });
    }
    }

    - + \ No newline at end of file diff --git a/docs/dev/unlocks/custom-unlocks/index.html b/docs/dev/unlocks/custom-unlocks/index.html index cd5e5f45..5804685d 100644 --- a/docs/dev/unlocks/custom-unlocks/index.html +++ b/docs/dev/unlocks/custom-unlocks/index.html @@ -5,13 +5,13 @@ Custom Unlocks | RogueLibs Documentation - +

    Custom Unlocks

    Custom unlocks allow your custom content to be accessed through vanilla menus. Unlike other hooks, unlocks persist throughout the game, and get destroyed and created only when initially loading the game or changing save slots. RogueLibs also creates wrappers around vanilla unlocks, to ensure the compatibility of your unlocks with the vanilla ones.

    UnlockWrapper is a flexible wrapper around a Unlock class, and provides methods to get the unlock's name, description, image and other stuff. It is the base class for any and all custom or vanilla unlocks.

    DisplayedUnlock derives from the UnlockWrapper class. It provides methods to display the unlock in the in-game menus. You can alter or augment your unlock's name and description, and even use Unity's rich text formatting. Note, that all rich text formatting tags have to be closed (this was introduced in one of the Unity versions).

    caution

    You probably shouldn't implement UnlockWrapper or DisplayedUnlock directly. Use the classes described below.

    Unlock classes

    RogueLibs provides the following classes that you can derive from:

    • ItemUnlock - for items;
    • AbilityUnlock - for abilities;
    • TraitUnlock - for traits;
    • MutatorUnlock - for mutators;
    • AgentUnlock - for agents;
    • BigQuestUnlock - for agent Big Quests;
    • ExtraUnlock - for achievements and other stuff;
    • FloorUnlock - for floor unlocks;
    note

    There's also a couple of classes that are in RogueLibs only for compatibility reasons.

    Initialization

    You can initialize your unlocks like this:

    MyCustomItem.cs
    public class MyCustomItem : CustomItem
    {
    [RLSetup]
    public static void Setup()
    {
    RogueLibs.CreateCustomItem<MyCustomItem>()
    .WithName(new CustomNameInfo("My Custom Item"))
    .WithDescription(new CustomNameInfo("My Custom Item is very cool and does a lot of great stuff"))
    .WithSprite(Properties.Resources.MyCustomItem)
    .WithUnlock(new ItemUnlock
    {
    UnlockCost = 10,
    CharacterCreationCost = 5,
    LoadoutCost = 4,
    });
    }
    }

    Or you can just initialize them directly (like in case of mutators):

    RogueLibs.CreateCustomUnlock(new MutatorUnlock("MyMutator"))
    .WithName(new CustomNameInfo("Mutator Name"))
    .WithDescription(new CustomNameInfo("Mutator Description"));
    - + \ No newline at end of file diff --git a/docs/intro/index.html b/docs/intro/index.html index 84380d92..4f670c2a 100644 --- a/docs/intro/index.html +++ b/docs/intro/index.html @@ -5,13 +5,13 @@ Introduction | RogueLibs Documentation - +

    Introduction

    Why should you use RogueLibs?

    • Because it's really easy to use! Just check out some examples in here.

    • Because the game is really messy and it's an absolute hell to patch in even a single item! In case of items or traits, everything RogueLibs is asking of you is a class implementing a couple of interfaces.

    • There are no alternatives... You've got no choice. 😈


    Check out the installation guide here!

    Troubleshooting

    If you can't get BepInEx or some mods to work, see the Troubleshooting section.

    If that didn't help, join SoR's Discord and ask for help in the #🔧|modding channel.

    There's also a Getting Started guide for plugin developers!


    Streets of Rogue's Discord:

    - + \ No newline at end of file diff --git a/docs/site/components/InventoryGrid/index.html b/docs/site/components/InventoryGrid/index.html index 93b94977..b5bd5b10 100644 --- a/docs/site/components/InventoryGrid/index.html +++ b/docs/site/components/InventoryGrid/index.html @@ -5,13 +5,13 @@ InventoryGrid | RogueLibs Documentation - +

    InventoryGrid

    Props

    import { Props as SlotProps } from '@site/src/components/InventorySlot';
    import { SelectorParameters } from '@site/src/components/hooks/useSelector';

    export type Props = SelectorParameters & {
    items?: (SlotProps | SlotProps[])[], // (not recommended) props of slots to display
    children?: React.ReactNode, // InventorySlot and InventoryRow children
    height?: number, // minimum height of the grid
    width?: number, // minimum width of the grid

    onClick?: (e: GridSlotArgs) => void, // click event handler
    interactive?: boolean, // determines whether slots can be selected
    }
    export type GridSlotArgs = {
    uid: string | undefined, // Unique Identifier of the clicked slot
    row: number, // index of the clicked slot's row
    column: number, // index of the clicked slot's column
    }

    Typical usage

    <InventoryGrid>
    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventoryRow>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    </InventoryGrid>
    25
    $1325
    25$13
    25

    Minimum width and height

    <InventoryGrid width={5} height={3}>
    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventoryRow>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    </InventoryGrid>
    25
    $1325
    25$13
    25

    onClick event

    <InventoryGrid interactive={true} onClick={e => console.log(e)}>
    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventoryRow>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    </InventoryGrid>
    25
    $1325
    25$13
    25

    note

    See the output in Developer Tools > Console.

    Interactive

    interactive={true} makes all of the slots hoverable, and allows you to use useSelector parameters.

    To make slots selectable you'll have to assign uids to them.

    info

    See useSelector for more info.

    <InventoryGrid interactive={true} defaultValues={["1", "3"]}
    minChoices={1} maxChoices={3} lockChoices={true} onChange={e => console.log(e)}>
    <InventorySlot uid="1" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot uid="2" sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventoryRow>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot uid="5" sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot uid="6" sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    </InventoryGrid>
    25
    $1325
    25$13
    25

    - + \ No newline at end of file diff --git a/docs/site/components/InventoryRow/index.html b/docs/site/components/InventoryRow/index.html index 74adcf62..fea8a8e1 100644 --- a/docs/site/components/InventoryRow/index.html +++ b/docs/site/components/InventoryRow/index.html @@ -5,13 +5,13 @@ InventoryRow | RogueLibs Documentation - +

    InventoryRow

    Props

    import { Props as SlotProps } from '@site/src/components/InventorySlot';
    import { SelectorParameters } from '@site/src/components/hooks/useSelector';

    export type Props = SelectorParameters & {
    items?: SlotProps[], // (not recommended) props of slots to display
    children?: React.ReactNode, // InventorySlot children to display
    width?: number, // minimum width of the row

    type?: "normal" | "toolbar", // type of the row
    onClick?: (e: RowSlotArgs) => void, // click event handler
    interactive?: boolean, // determines whether slots can be selected
    }
    export type RowSlotArgs = {
    uid: string | undefined, // Unique Identifier of the clicked slot
    index: number, // index of the clicked slot
    }

    Typical usage

    <InventoryRow>
    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    Minimum width

    If the provided amount of slots is less than the specified width, the remaining space is filled with empty slots:

    <InventoryRow width={8}>
    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    Row types

    There are two types of rows: "normal" (default) and "toolbar":

    <InventoryRow type="normal">
    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    <br/>
    <InventoryRow type="toolbar">
    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    1
    225
    325
    4$13
    5
    6

    info

    As you can see, "toolbar" overrides slots' tooltips and adds a blue outline.

    onClick event

    <InventoryRow interactive={true} onClick={e => console.log(e)}>
    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    note

    See the output in Developer Tools > Console.

    Interactive

    interactive={true} makes all of the slots hoverable, and allows you to use useSelector parameters.

    To make slots selectable you'll have to assign uids to them.

    info

    See useSelector for more info.

    <InventoryRow interactive={true} defaultValues={["1", "3"]}
    minChoices={1} maxChoices={3} lockChoices={true} onChange={e => console.log(e)}>
    <InventorySlot uid="1" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot uid="2" sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot uid="5" sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot uid="6" sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    - + \ No newline at end of file diff --git a/docs/site/components/InventorySlot/index.html b/docs/site/components/InventorySlot/index.html index d6e0f473..9d3cb327 100644 --- a/docs/site/components/InventorySlot/index.html +++ b/docs/site/components/InventorySlot/index.html @@ -5,13 +5,13 @@ InventorySlot | RogueLibs Documentation - +

    InventorySlot

    Props

    export type Props = {
    sprite?: string, // sprite of the item
    tooltip?: string | number, // tooltip text
    tooltipColor?: string, // tooltip text color (default: #FFED00)
    count?: string | number, // count text
    countColor?: string, // count text color (default: #FFFFFF)

    type?: "normal" | "selected" | "locked" | null, // type of the slot
    onClick?: () => void, // click event handler
    hoverable?: boolean, // determines whether to change the slot image, when hovered over
    cantClick?: boolean, // determines whether the slot cannot be clicked/interacted with

    uid?: string, // Unique Identifier (see InventoryRow and InventoryGrid for more info)
    }

    Typical usages

    <InventorySlot sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    25
    $1325

    With colors

    <InventorySlot sprite={useBaseUrl("/img/Generic5.png")}
    count={25} countColor="#552299"
    tooltip="$13" tooltipColor="#9922BB77"/>
    $1325

    Slot types

    <InventorySlot type="normal"/>
    <InventorySlot type="selected"/>
    <InventorySlot type="locked"/>
    <InventorySlot type={null}/>
    <br/>
    <InventorySlot type="normal" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot type="selected" sprite={useBaseUrl("/img/Generic2.png")}/>
    <InventorySlot type="locked" sprite={useBaseUrl("/img/Generic3.png")}/>
    <InventorySlot type={null} sprite={useBaseUrl("/img/Generic4.png")}/>


    onClick event

    <InventorySlot sprite={useBaseUrl("/img/Generic4.png")} hoverable={true} count={25} tooltip="$13"
    onClick={() => console.log("Just clicked on the inventory slot!")}/>
    $1325

    note

    See the output in Developer Tools > Console.

    Hoverable

    info

    It's usually used inside other components to make them look nicer, but you can use it yourself too.

    <InventorySlot hoverable={true} type="normal"/>
    <InventorySlot hoverable={true} type="selected"/>
    <InventorySlot hoverable={true} type="locked"/>
    <InventorySlot hoverable={true} type={null}/>
    <br/>
    <InventorySlot hoverable={true} type="normal" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot hoverable={true} type="selected" sprite={useBaseUrl("/img/Generic2.png")}/>
    <InventorySlot hoverable={true} type="locked" sprite={useBaseUrl("/img/Generic3.png")}/>
    <InventorySlot hoverable={true} type={null} sprite={useBaseUrl("/img/Generic4.png")}/>


    Cant click

    info

    It's usually used inside other components to make them look nicer, but you can use it yourself too.

    <InventorySlot hoverable={true} type="selected"/>
    <InventorySlot hoverable={true} cantClick={true} type="selected"/>
    <InventorySlot hoverable={true} type="locked"/>
    <InventorySlot hoverable={true} cantClick={true} type="locked"/>
    <br/>
    <InventorySlot hoverable={true} type="selected" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot hoverable={true} cantClick={true} type="selected" sprite={useBaseUrl("/img/Generic2.png")}/>
    <InventorySlot hoverable={true} type="locked" sprite={useBaseUrl("/img/Generic3.png")}/>
    <InventorySlot hoverable={true} cantClick={true} type="locked" sprite={useBaseUrl("/img/Generic4.png")}/>


    - + \ No newline at end of file diff --git a/docs/site/hooks/useSelector/index.html b/docs/site/hooks/useSelector/index.html index 433e0402..642ef26e 100644 --- a/docs/site/hooks/useSelector/index.html +++ b/docs/site/hooks/useSelector/index.html @@ -5,7 +5,7 @@ useSelector | RogueLibs Documentation - + @@ -14,7 +14,7 @@ maxChoices specifies the maximum amount of selected slots. -1 ≡ ∞.

    <InventoryRow interactive={true} minChoices={1} maxChoices={3}>
    <InventorySlot uid="1" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot uid="2" sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot uid="5" sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot uid="6" sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    defaultValues

      defaultValues?: string | string[] | (() => string | string[] | undefined),

    defaultValues can be a single string, an array of strings or a lazy function.

    <InventoryRow interactive={true} maxChoices={3} defaultValues={["2", "4"]}>
    <InventorySlot uid="1" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot uid="2" sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot uid="5" sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot uid="6" sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    If not set, or if the lazy function returns undefined, picks the first minChoices values:

    <InventoryRow interactive={true} minChoices={3} maxChoices={-1}>
    <InventorySlot uid="1" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot uid="2" sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot uid="5" sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot uid="6" sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    lockChoices

    You can "lock" the selected values with lockChoices.
    When the maximum selectable amount is reached, non-selected slots change to "locked" type.

    <InventoryRow interactive={true} maxChoices={2} lockChoices={true}>
    <InventorySlot uid="1" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot uid="2" sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot uid="5" sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot uid="6" sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    group

    You can use group to save the selected values in the local storage.
    Try selecting some slots and then reloading the page.

    <InventoryRow interactive={true} group="useSelectorDemo" maxChoices={4}>
    <InventorySlot uid="1" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot uid="2" sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot uid="5" sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot uid="6" sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    25

    Pro-tip: Local Storage

    See the stored data in Developer Tools > Application > Storage > Local Storage.

    You can also use group to synchronize choices between different instances of components:

    <InventoryRow interactive={true} group="useSelectorDemo2" maxChoices={2}>
    <InventorySlot uid="1" sprite={useBaseUrl("/img/Generic.png")}/>
    <InventorySlot uid="2" sprite={useBaseUrl("/img/Generic2.png")} count={25}/>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot type={null}/>
    <InventorySlot type={null}/>
    </InventoryRow>
    <InventoryRow interactive={true} group="useSelectorDemo2" maxChoices={2}>
    <InventorySlot type={null}/>
    <InventorySlot type={null}/>
    <InventorySlot uid="3" sprite={useBaseUrl("/img/Generic3.png")} count={25} tooltip="$13"/>
    <InventorySlot uid="4" sprite={useBaseUrl("/img/Generic4.png")} count="$13" tooltip={25}/>
    <InventorySlot uid="5" sprite={useBaseUrl("/img/Generic5.png")} tooltip={25}/>
    <InventorySlot uid="6" sprite={useBaseUrl("/img/Generic6.png")}/>
    </InventoryRow>
    25
    $1325
    25$13
    $1325
    25$13
    25

    note

    Note the behavior, when a slot that doesn't exist in one of the rows is selected in another row.

    onChange event

    onChange doesn't necessarily mean that the values changed. Just that they could be different.

    - + \ No newline at end of file diff --git a/docs/site/hooks/useStorage/index.html b/docs/site/hooks/useStorage/index.html index 18cd3162..b9ba7936 100644 --- a/docs/site/hooks/useStorage/index.html +++ b/docs/site/hooks/useStorage/index.html @@ -5,14 +5,14 @@ useStorage | RogueLibs Documentation - +

    useStorage

    useStorage is used to store strings in the browser's local storage.
    It also synchronizes all instances using the same storage slot.

    Signature

    function (
    slotName: string | null, // name of the storage slot to use
    defaultValue?: string | null | (() => string | null), // default value of the storage slot
    onChange?: (value: string | null) => void // change event handler
    ): [
    string | null, // current value (null ≡ the storage slot doesn't exist)
    React.Dispatch<React.SetStateAction<string | null>> // function to set the value
    ]

    Usage

    info

    If slotName is null, the hook works just like a useState.

    import useStorage from "../hooks/useStorage";

    export default function ({/* your props */}: Props) {

    const [value, setValue] = useStorage("my.storage.slot", null);

    /* ... */
    }

    caution

    The value is read from the local storage in an useEffect hook, so, on the first render, the value will be the default one.

    - + \ No newline at end of file diff --git a/docs/site/hooks/useStorageArray/index.html b/docs/site/hooks/useStorageArray/index.html index e65d1ca1..09b54f12 100644 --- a/docs/site/hooks/useStorageArray/index.html +++ b/docs/site/hooks/useStorageArray/index.html @@ -5,7 +5,7 @@ useStorageArray | RogueLibs Documentation - + @@ -13,7 +13,7 @@

    useStorageArray

    useStorageArray is used to store strings in the browser's local storage.
    It also synchronizes all instances using the same storage slot.

    info

    It works just like useStorage but with string arrays instead of single strings.
    It joins the stored strings with a semicolon (;), like this: value1;value2;value3.

    Signature

    function (
    slotName: string | null, // name of the storage slot to use
    defaultValues?: string[] | (() => string[] | undefined), // default values of the storage slot
    onChange?: (values: string[]) => void // change event handler
    ): [
    string[], // current values
    React.Dispatch<React.SetStateAction<string[]>> // function to set the values
    ]

    Usage

    info

    If slotName is null, the hook works just like a useState.

    import useStorageArray from "../hooks/useStorageArray";

    export default function ({/* your props */}: Props) {

    const [values, setValues] = useStorageArray("my.storage.slot", []);

    /* ... */
    }

    caution

    The values are read from the local storage in an useEffect hook, so, on the first render, the values will be the default ones.

    - + \ No newline at end of file diff --git a/docs/site/index.html b/docs/site/index.html index a4fd9aea..6302b8d8 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -5,13 +5,13 @@ Components Index | RogueLibs Documentation - +
    - + \ No newline at end of file diff --git a/docs/site/intro/index.html b/docs/site/intro/index.html index e91f3337..1be9e46e 100644 --- a/docs/site/intro/index.html +++ b/docs/site/intro/index.html @@ -5,7 +5,7 @@ Introduction | RogueLibs Documentation - + @@ -14,7 +14,7 @@ Replace [[PROJECT-NAME]] with the name of your project.

    Then open that workspace file, go to Search and replace the following in all files:

    • [[PROJECT-NAME]] - the name of your project.
    • [[USERNAME]] - your username on GitHub.
    • [[REPOSITORY]] - your project's repository name on GitHub.
    • [[MAIN-BRANCH]] - your repository's default branch (master or main).
    • [[LOGO-ALT-TEXT]] - alt text, describing your logo.
    • [[EMAIL]] - your e-mail (required for the workflow).

    Also replace the images in static/img: favicon.ico, logo.png and logo-dark.png. (.ico converter)

    Then move the .github directory up one level, so it's in the root of your repository and push the changes.

    Deploying the website

    note

    I find Docusaurus docs on that a little bit confusing, so here's my version of these instructions.

    Generate a SSH key

    1. Open the command line and generate a SSH key: ssh-keygen -t ed25519 -C "[[EMAIL]]";
    2. When you're prompted to "Enter a file in which to save the key," press Enter. This accepts the default file location;
    3. When you're prompted to "Enter a passphrase," press Enter (no passphrase);
    4. When you're prompted to repeat the passphrase, press Enter again;

    You'll find the keys in your profile's directory (C:\Users\[[UserName]]\.ssh or ~/.ssh).

    Create a deploy key on GitHub

    1. Go to your GitHub repository's Settings > Deploy Keys > Add deploy key;
    2. Paste the contents of the id_ed25519.pub file;
    3. Check the "Allow write access" box;
    4. Save (the key's name doesn't matter, you can call it DeployKey1 or something like that);

    Create a GitHub secret

    1. Go to your GitHub repository's Settings > Secrets > New repository secret;
    2. Paste the contents of the id_ed25519 file;
    3. Call the secret GH_PAGES_DEPLOY;
    4. Save your secret;

    Trigger the workflow

    1. Push the website to the repository, if you didn't already;
    2. Go to your GitHub repository's Actions > documentation.yml;
    3. If the workflow didn't trigger on its own, start it.
    note

    If there aren't any errors, the website will be deployed at [[USERNAME]].github.io/[[REPOSITORY]]/.
    If there are errors, you'll see them in the details of the workflow run.

    Adding components

    Clone or download the RogueLibs' repository and copy-paste the components from there into your website.

    Components probably won't be versioned, so you'll have to look for updates yourself.

    - + \ No newline at end of file diff --git a/docs/user/installation/index.html b/docs/user/installation/index.html index 2e60e334..78a2e273 100644 --- a/docs/user/installation/index.html +++ b/docs/user/installation/index.html @@ -5,13 +5,13 @@ Installation | RogueLibs Documentation - +

    Installation

    To start using mods, first you need to install BepInEx, the modding framework that we're using to mod Streets of Rogue. Then, install RogueLibs (both plugin and patcher - two different important files) and then any mods that you want to play with. You can find a lot of mods on SoR ModHub or GameBanana.

    Installing BepInEx

    If you already have BepInEx installed, skip this step.

    Downloading BepInEx

    Go here and download the version for your OS:

    Download the version of BepInEx for your game's executable's type:

    • BepInEx_x64_5.x.y.z.zip is for 64-bit executables.
    • BepInEx_x86_5.x.y.z.zip is for 32-bit executables.

    Here's a few pointers:

    • You might notice some files in the game's root directory: Galaxy64.dll, UnityCrashHandler64.exe. If you have these, then I'm pretty sure it means that the game's executable is also 64-bit.
    • Launch the game and open the Task Manager. If you see "(32 bit)" in the name of the game's process, then it's 32-bit; otherwise, it's 64-bit.

    BepInEx v6

    BepInEx v6 pre-release recently came out. Don't use it yet. All of the mods at the moment still use BepInEx v5.4.x, and if you install v6, the mods probably won't work. BepInEx developers plan on supporting older v5 plugins in the future, so once a stable v6 version comes out, you should be able to use it.

    Extracting files

    Open Steam's game library, right-click on the game and click Properties...:

    Go to Local files and click Browse...:


    Extract the contents of the .zip file into the game's root directory.

    Make sure you extract it the right way. There's a lot of wrong ways, apparently.

    • Make sure that BepInEx folder is in the same directory as your game's executable (StreetsOfRogue.exe);
    • Make sure that you extract the doorstop_config.ini and winhttp.dll files too;

    Running the game

    note

    You need to run the game at least once, so that BepInEx can generate config files and directories!

    Just run the game. Either through an executable, or any game launcher.

    You can close the game once you see a logo or a loading screen.


    Running BepInEx through Steam

    On Windows you can run BepInEx through Steam too. Nothing to worry about.


    Installing RogueLibs

    Download the latest RogueLibs version

    You need to download only two files: RogueLibsCore.dll and RogueLibsPatcher.dll.

    Go to the BepInEx's directory and put RogueLibsCore.dll file into plugins directory:

    Put RogueLibsPatcher.dll file into patchers directory:

    caution

    The directory is called patchers, it's different from plugins.


    Installing plugins (mods)

    Download the mods that you want to install (.dll files).

    And put these .dll files in the BepInEx/plugins directory.

    Where can I get mods?

    SoR ModHub is a collection of mods that use the latest RogueLibs v3 that I know about. Includes links to other mod sources as well, and is updated pretty regularly.

    GameBanana - Streets of Rogue's official mod-sharing place. Some mods don't get released or updated there, since the process of setting up or updating a page is way too long and overly complicated.

    The #👍|modding-gallery🔧 channel in the official SoR's Discord. You'll find all of the latest info, updates and bugfixes there. It's kind of hard to search for specific mods though.

    ModDB and NexusMods are barely used at all.

    - + \ No newline at end of file diff --git a/docs/user/troubleshooting/index.html b/docs/user/troubleshooting/index.html index fcd98131..161390a3 100644 --- a/docs/user/troubleshooting/index.html +++ b/docs/user/troubleshooting/index.html @@ -5,13 +5,13 @@ Frequently Encountered Problems | RogueLibs Documentation - +

    Frequently Encountered Problems

    BepInEx is not working

    I only extracted the BepInEx folder from the archive, and BepInEx doesn't work.

    I have Windows and I installed BepInEx for Linux/macOS (unix).

    I have Linux/macOS and I installed BepInEx for Windows (x64/x86).

    I have Windows and the game's executable is x32/x64 and I installed BepInEx for the wrong version (x64/x32).
    I installed BepInEx correctly, but it still doesn't work.

    RogueLibs is not working

    I put both RogueLibsCore.dll and RogueLibsPatcher.dll in BepInEx/plugins and RogueLibs doesn't work.

    I have both v2 and v3 versions of RogueLibs installed.
    I installed RogueLibs correctly and it still doesn't work.

    Plugins are not working

    I have mods that were made with RogueLibs v2 and they're not working with RogueLibs v3.
    I installed the mods without paying attention to their dependencies, and they don't work.
    I installed the mods correctly but they don't work.
    - + \ No newline at end of file diff --git a/index.html b/index.html index 457bc372..234d7aae 100644 --- a/index.html +++ b/index.html @@ -5,13 +5,13 @@ RogueLibs - SoR Modding Library - +

    Redefining Limits.

    Custom Sprites

    RogueLibs allows you to add your own and modify existing tk2d/Unity sprites!

    Custom Interactions

    Ever wondered why SoR doesn't have certain interactions? It's time for you to fill in the blanks!

    Custom Items

    Adding custom items with various usages and effects could not be easier with RogueLibs!

    Custom Mutators

    Mutators are pretty cool!

    Custom Abilities

    Yep, RogueLibs also has custom abilities!

    Custom Traits

    And traits too!

    - + \ No newline at end of file diff --git a/test/index.html b/test/index.html index e35d3e57..a854dfad 100644 --- a/test/index.html +++ b/test/index.html @@ -5,13 +5,13 @@ Test Page | RogueLibs Documentation - +

    Test Page

    Header Test

    Item Name
    Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description. Item Description.
    Unlock Cost: 10
    CCPV: 5
    More details. More details. More details. More details. More details. More details. More details. More details. More details.
    - + \ No newline at end of file