├── elixir ├── __init__.py ├── cli.py ├── processors.py ├── compilers.py └── rbxmx.py ├── examples ├── module-detection │ ├── src │ │ ├── Server.lua │ │ └── Module.lua │ ├── build.py │ └── README.md ├── using-processors │ ├── src │ │ └── Nevermore │ │ │ ├── Modules │ │ │ ├── Server.Main.lua │ │ │ └── Client.Main.lua │ │ │ └── App │ │ │ ├── NevermoreEngineLoader.lua │ │ │ └── NevermoreEngine.lua │ ├── build.py │ └── README.md └── sample-project │ ├── src │ ├── SayHello.lua │ └── Modules │ │ └── Hello.lua │ ├── build.py │ └── README.md ├── screenshots ├── example-compiled-source.png └── example-model-contents.png ├── .editorconfig ├── setup.py ├── .gitignore ├── LICENSE ├── tests ├── test_processors.py └── test_rbxmx.py └── README.md /elixir/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/module-detection/src/Server.lua: -------------------------------------------------------------------------------- 1 | local module = require(script.Parent.Module) 2 | 3 | module.hello() 4 | -------------------------------------------------------------------------------- /examples/using-processors/src/Nevermore/Modules/Server.Main.lua: -------------------------------------------------------------------------------- 1 | -- ClassName: Script 2 | 3 | print("Hello World!") 4 | -------------------------------------------------------------------------------- /examples/sample-project/src/SayHello.lua: -------------------------------------------------------------------------------- 1 | local hello = require(script.Parent.Modules.Hello) 2 | 3 | print(hello.greet()) 4 | -------------------------------------------------------------------------------- /screenshots/example-compiled-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocksel/elixir/HEAD/screenshots/example-compiled-source.png -------------------------------------------------------------------------------- /screenshots/example-model-contents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vocksel/elixir/HEAD/screenshots/example-model-contents.png -------------------------------------------------------------------------------- /examples/module-detection/src/Module.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | function module.hello() 4 | return "Hi!" 5 | end 6 | 7 | return module 8 | -------------------------------------------------------------------------------- /examples/sample-project/src/Modules/Hello.lua: -------------------------------------------------------------------------------- 1 | local hello = {} 2 | 3 | function hello.greet() 4 | return "Hello, World!" 5 | end 6 | 7 | return hello 8 | -------------------------------------------------------------------------------- /examples/module-detection/build.py: -------------------------------------------------------------------------------- 1 | from elixir.compilers import ModelCompiler 2 | 3 | source = "src/" 4 | dest = "model.rbxmx" 5 | 6 | compiler = ModelCompiler(source, dest) 7 | compiler.compile() 8 | -------------------------------------------------------------------------------- /examples/sample-project/build.py: -------------------------------------------------------------------------------- 1 | from elixir.compilers import ModelCompiler 2 | 3 | source = "src/" 4 | dest = "model.rbxmx" 5 | 6 | compiler = ModelCompiler(source, dest) 7 | compiler.compile() 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.py] 10 | indent_style = space 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /examples/using-processors/build.py: -------------------------------------------------------------------------------- 1 | from elixir.compilers import ModelCompiler 2 | from elixir.processors import NevermoreProcessor 3 | 4 | source = "src/" 5 | dest = "model.rbxmx" 6 | 7 | compiler = ModelCompiler(source, dest, processor=NevermoreProcessor) 8 | compiler.compile() 9 | -------------------------------------------------------------------------------- /examples/module-detection/README.md: -------------------------------------------------------------------------------- 1 | ## Module Detection 2 | 3 | Elixir will automatically detects if a Lua file is a normal Script or a 4 | ModuleScript, so you don't need to bother with setting properties in most cases. 5 | 6 | If you run `python build.py` you can see how `Server.lua` is compiled to a 7 | Script, and `Module.lua` is compiled to a ModuleScript, without explicitly 8 | telling Elixir that. 9 | -------------------------------------------------------------------------------- /examples/sample-project/README.md: -------------------------------------------------------------------------------- 1 | ## Sample Project 2 | 3 | This is a barebones project template for you to get started using Elixir. 4 | 5 | The `src/` directory contains a couple lua files to give you an idea of how 6 | things get compiled. Run `python build.py` and import the compiled model into 7 | your game to have a look. 8 | 9 | This should be enough to get you started using Elixir. If not, feel free to 10 | submit an issue. 11 | -------------------------------------------------------------------------------- /examples/using-processors/src/Nevermore/Modules/Client.Main.lua: -------------------------------------------------------------------------------- 1 | -- ClassName: LocalScript 2 | 3 | local replicatedStorage = game:GetService("ReplicatedStorage") 4 | local nevermore = require(replicatedStorage:WaitForChild("NevermoreEngine")) 5 | 6 | local player = game.Players.LocalPlayer 7 | 8 | -- The splash screen has to be removed manually, otherwise it will stay 9 | -- on-screen indefinitely. 10 | nevermore.ClearSplash() 11 | 12 | print("Hello "..player.Name.."!") 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="elixir", 5 | version="2.0.0", 6 | description="Turn directories and Lua files into a ROBLOX compatible XML file.", 7 | author="David Minnerly", 8 | license="MIT", 9 | classifiers=[ 10 | "Development Status :: 4 - Beta", 11 | "Intended Audience :: Developers", 12 | "Natural Language :: English", 13 | "Programming Language :: Python :: 3.4", 14 | ], 15 | keywords="lua roblox compiler", 16 | packages=["elixir"], 17 | install_requires=[ 18 | "docopt" 19 | ], 20 | entry_points={ 21 | "console_scripts": [ "elixir=elixir.cli:main" ] 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # py.test 2 | .cache/ 3 | 4 | # Ignore any compiled model files 5 | *.rbxmx 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | -------------------------------------------------------------------------------- /examples/using-processors/README.md: -------------------------------------------------------------------------------- 1 | ## Using Processors 2 | 3 | This is an example project that shows you how to use alternative processors when 4 | compiling. 5 | 6 | Peruse `elixir.processors` to see the available processor classes. From there 7 | you can pick the one you want and import it. Then you pass it as the `processor` 8 | argument to the compiler. 9 | 10 | ```python 11 | from elixir.compilers import ModelCompiler 12 | from elixir.processors import NevermoreProcessor 13 | 14 | source = "src/" 15 | dest = "model.rbxmx" 16 | 17 | compiler = ModelCompiler(source, dest, processor=NevermoreProcessor) 18 | compiler.compile() 19 | ``` 20 | 21 | This example is using an older version of Nevermore and its legacy processor. This is the only other processor, so it is being used to showcase how different processors can work. 22 | 23 | If you're creating a Nevermore project, remember to grab the latest version from 24 | [Quenty/NevermoreEngine](https://github.com/Quenty/NevermoreEngine/). The ones 25 | provided in the example could become outdated, don't rely on them for your game. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 David Minnerly 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /elixir/cli.py: -------------------------------------------------------------------------------- 1 | """Elixir 2 | 3 | Usage: 4 | elixir [options] 5 | elixir -h | --help 6 | 7 | Options: 8 | -h, --help Show this screen. 9 | -m, --model-name The name of the top-level container folder (default: 10 | folder name of the `source` argument.) 11 | -p, --processor Use an engine when compiling. 12 | """ 13 | 14 | import os 15 | import os.path 16 | 17 | from docopt import docopt 18 | 19 | from elixir.compilers import ModelCompiler 20 | import elixir.processors 21 | 22 | def get_processor(processor_name): 23 | """Gets a processor by its name. 24 | 25 | This allows the user to reference one of the processors from the command 26 | line. Since you can't directly get the class itself from a command, we need 27 | to use a string. 28 | 29 | processor_name : str 30 | The name of one of the available processors. 31 | """ 32 | 33 | if processor_name in dir(elixir.processors): 34 | return getattr(elixir.processors, processor_name) 35 | 36 | def main(): 37 | args = docopt(__doc__) 38 | 39 | source = os.path.abspath(args[""]) 40 | dest = os.path.abspath(args[""]) 41 | model_name = args["--model-name"] 42 | processor = args["--processor"] or "BaseProcessor" 43 | 44 | compiler = ModelCompiler(source, dest, model_name=model_name, 45 | processor=get_processor(processor)) 46 | compiler.compile() 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /tests/test_processors.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | from elixir import processors 4 | 5 | class TestGettingFileContents: 6 | def test_can_get_file_contents(self, tmpdir): 7 | f = tmpdir.join("file.txt") 8 | f.write("Content") 9 | 10 | content = processors._get_file_contents(str(f)) 11 | 12 | assert content == "Content" 13 | 14 | def test_does_not_error_if_path_does_not_exist(self): 15 | fake_path = "/a/b/c" 16 | content = processors._get_file_contents(fake_path) 17 | 18 | assert content is None 19 | 20 | class TestBaseProcessor: 21 | processor = processors.BaseProcessor() 22 | 23 | def test_processor_is_detecting_modules(self): 24 | content = dedent("""\ 25 | local function hello() 26 | return "Hello, World!" 27 | end 28 | 29 | return hello""") 30 | 31 | class_name = processors._get_script_class(content) 32 | 33 | assert class_name == "ModuleScript" 34 | 35 | class TestNevermoreProcessor: 36 | processor = processors.NevermoreProcessor() 37 | 38 | def test_engine_loader_is_enabled(self): 39 | """Handling for the main Nevermore loader. 40 | 41 | When we encounter `NevermoreEngineLoader.lua` we need to make sure a 42 | non-disabled Script is output. 43 | """ 44 | 45 | name = "NevermoreEngineLoader" 46 | script = self.processor.process_script(name, "") 47 | 48 | assert script.disabled.text == "false" 49 | 50 | def test_all_other_scripts_are_disabled(self): 51 | script = self.processor.process_script("Script", "") 52 | 53 | assert script.disabled.text == "true" 54 | -------------------------------------------------------------------------------- /examples/using-processors/src/Nevermore/App/NevermoreEngineLoader.lua: -------------------------------------------------------------------------------- 1 | --- This scripts loads Nevermore from the server. 2 | -- It also replicates the into ReplicatedStorage for internal usage. 3 | 4 | ----------------------- 5 | -- UTILITY FUNCTIONS -- 6 | ----------------------- 7 | 8 | local TestService = game:GetService('TestService') 9 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 10 | 11 | local function WaitForChild(Parent, Name, TimeLimit) 12 | -- Waits for a child to appear. Not efficient, but it shoudln't have to be. It helps with debugging. 13 | -- Useful when ROBLOX lags out, and doesn't replicate quickly. 14 | -- @param TimeLimit If TimeLimit is given, then it will return after the timelimit, even if it hasn't found the child. 15 | 16 | assert(Parent ~= nil, "Parent is nil") 17 | assert(type(Name) == "string", "Name is not a string.") 18 | 19 | local Child = Parent:FindFirstChild(Name) 20 | local StartTime = tick() 21 | local Warned = false 22 | 23 | while not Child and Parent do 24 | wait(0) 25 | Child = Parent:FindFirstChild(Name) 26 | if not Warned and StartTime + (TimeLimit or 5) <= tick() then 27 | Warned = true 28 | warn("Infinite yield possible for WaitForChild(" .. Parent:GetFullName() .. ", " .. Name .. ")") 29 | if TimeLimit then 30 | return Parent:FindFirstChild(Name) 31 | end 32 | end 33 | end 34 | 35 | if not Parent then 36 | warn("Parent became nil.") 37 | end 38 | 39 | return Child 40 | end 41 | 42 | ------------- 43 | -- LOADING -- 44 | ------------- 45 | 46 | -- Wait for parent to resolve 47 | while not script.Parent do 48 | wait(0) 49 | end 50 | 51 | -- Identify the modular script 52 | local NevermoreModularScript = ReplicatedStorage:FindFirstChild("NevermoreEngine") 53 | if not NevermoreModularScript then 54 | local NevermoreModularScriptSource = WaitForChild(script.Parent, "NevermoreEngine") 55 | NevermoreModularScript = NevermoreModularScriptSource:Clone() 56 | NevermoreModularScript.Archivable = false 57 | end 58 | 59 | local Nevermore = require(NevermoreModularScript) 60 | 61 | -- Set identifier, and initiate. 62 | Nevermore.Initiate() 63 | NevermoreModularScript.Parent = ReplicatedStorage 64 | -------------------------------------------------------------------------------- /elixir/processors.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | from elixir import rbxmx 4 | 5 | def _get_file_contents(path): 6 | if os.path.isfile(path): 7 | with open(path) as f: 8 | return f.read() 9 | 10 | def _get_script_class(content): 11 | """Checks a file's content to determine the type of Lua Script it is.""" 12 | 13 | if rbxmx.is_module(content): 14 | return "ModuleScript" 15 | else: 16 | return "Script" 17 | 18 | class BaseProcessor: 19 | """The primary processor class. 20 | 21 | A processor is what compilers use to determine what happens when they 22 | encounter a file or folder. All of the `process` methods return a new 23 | instance from `elixir.rbxmx`. 24 | 25 | For example, when processing a file, we return a new Script. The XML of 26 | these instances is then appended into the hierarchy when compiling. 27 | """ 28 | 29 | def process_folder(self, name): 30 | """Processing for folders. 31 | 32 | name : str 33 | The name of the folder to process. 34 | """ 35 | 36 | return rbxmx.ContainerElement(name=name) 37 | 38 | def process_model(self, content): 39 | """Processing for ROBLOX Model files (.rbxmx). 40 | 41 | content : str 42 | The contents of the Model file. 43 | """ 44 | 45 | return rbxmx.ModelElement(content) 46 | 47 | def process_script(self, name, content): 48 | """Processing for Lua files. 49 | 50 | name : str 51 | The name of the Script. 52 | content : str 53 | The Lua source code. 54 | """ 55 | 56 | class_name = _get_script_class(content) 57 | 58 | script = rbxmx.ScriptElement(class_name, name=name, source=content) 59 | script.use_embedded_properties() 60 | 61 | return script 62 | 63 | def get_element(self, path): 64 | """Returns a Python instance representing a ROBLOX instance. 65 | 66 | This routes `path` to the most appropriate `process` method and returns 67 | one of the classes from `rbxmx`. 68 | 69 | path : str 70 | The path to a folder or file to be processed. 71 | """ 72 | 73 | name = os.path.basename(path) 74 | 75 | if os.path.isdir(path): 76 | return self.process_folder(name) 77 | else: 78 | name, ext = os.path.splitext(name) 79 | content = _get_file_contents(path) 80 | 81 | if ext == ".lua": 82 | return self.process_script(name, content) 83 | elif ext == ".rbxmx": 84 | return self.process_model(content) 85 | 86 | class NevermoreProcessor(BaseProcessor): 87 | """Processor for NevermoreEngine (Legacy). 88 | 89 | This should be only used on or before commit b9b5a8 (linked below). 90 | Nevermore was refactored and no longer requries this special handling. 91 | 92 | This processor is kept here for legacy support. 93 | 94 | https://github.com/Quenty/NevermoreEngine/tree/b9b5a836e4b5801ba19abfa2a5eab79921076542 95 | """ 96 | 97 | def process_script(self, name, content): 98 | script = super().process_script(name, content) 99 | 100 | # This is the name of the Script that loads Nevermore. It sets the 101 | # Disabled property to `false` for Scripts that it determines should 102 | # run. All Scripts need to be disabled by default for this handling. 103 | is_engine_loader = name == "NevermoreEngineLoader" 104 | 105 | is_module = script.element.attrib.get("class") == "ModuleScript" 106 | 107 | if not is_engine_loader and not is_module: 108 | script.disabled.text = "true" 109 | 110 | return script 111 | -------------------------------------------------------------------------------- /elixir/compilers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | from xml.etree import ElementTree 4 | 5 | from elixir import rbxmx 6 | from elixir.processors import BaseProcessor 7 | 8 | def create_path(path): 9 | parent_folders = os.path.dirname(path) 10 | if parent_folders and not os.path.exists(parent_folders): 11 | os.makedirs(parent_folders) 12 | 13 | class BaseCompiler: 14 | def __init__(self, source, dest): 15 | self.source = os.path.normpath(source) 16 | self.dest = os.path.normpath(dest) 17 | 18 | def compile(self): 19 | create_path(self.dest) 20 | 21 | class ModelCompiler(BaseCompiler): 22 | """Creates a ROBLOX Model from source code. 23 | 24 | It converts folders, Lua files, and ROBLOX models into an XML file that you 25 | can import into your game. 26 | 27 | Usage: 28 | 29 | # This is just getting paths to the source directory and the file that 30 | # we'll be outputting to. 31 | root = os.path.abspath(os.path.dirname(__file__)) 32 | source = os.path.join(root, "source") 33 | build = os.path.join(root, "build/output.rbxmx") 34 | 35 | # Compiles everything under `source/` to `build/output.rbxmx`. 36 | model = ModelCompiler(source, build) 37 | model.compile() 38 | 39 | Now you'll have a ROBLOX Model in `build/` that you can drag into your 40 | ROBLOX level. And just like that, all of your code is there! 41 | 42 | source : str 43 | The directory containing Lua code and ROBLOX Models that you want 44 | compiled. 45 | dest : str 46 | The name of the file that will be created when compiling. Directories in 47 | this path are automatically created for you. 48 | 49 | It's important that the file extension should either be `.rbxmx` or 50 | `.rbxm`. Those are the two filetypes recognized by ROBLOX Studio. You 51 | won't be able to import the file into your game otherwise. 52 | processor=None : BaseProcessor 53 | The processor to use when compiling. 54 | 55 | A processor is what handles files and folders as the compiler comes 56 | across them. It dictates the type of ROBLOX class is returned. 57 | 58 | For example, when BaseProcessor comes across a Lua file, it will return 59 | a new `elixir.rbx.Script` instance. 60 | """ 61 | 62 | def __init__(self, source, dest, processor=BaseProcessor): 63 | super().__init__(source, dest) 64 | 65 | self.processor = processor() 66 | 67 | def _create_hierarchy(self, path): 68 | """Turns a directory structure into ROBLOX-compatible XML. 69 | 70 | path : str 71 | The path to a directory to recurse through. 72 | """ 73 | 74 | root_xml = rbxmx.get_base_tag() 75 | 76 | def recurse(path, hierarchy): 77 | for item in os.listdir(path): 78 | item_path = os.path.join(path, item) 79 | 80 | element = self.processor.get_element(item_path) 81 | xml = element.get_xml() 82 | element.append_to(hierarchy) 83 | 84 | if os.path.isdir(item_path): 85 | recurse(item_path, xml) 86 | 87 | recurse(path, root_xml) 88 | 89 | return root_xml 90 | 91 | def _create_model(self): 92 | return self._create_hierarchy(self.source) 93 | 94 | def _write_model(self): 95 | """Compiles the model and writes it to the output file.""" 96 | 97 | # Writing as binary so that we can use UTF-8 encoding. 98 | with open(self.dest, "wb+") as f: 99 | model = self._create_model() 100 | tree = ElementTree.ElementTree(model) 101 | 102 | # ROBLOX does not support self-closing tags. In the event that an 103 | # element is blank (eg. a script doesn't have any contents) you 104 | # won't be able to import the model. We need to ensure all elements 105 | # have an ending tag by setting `short_empty_elements=False`. 106 | tree.write(f, encoding="utf-8", short_empty_elements=False) 107 | 108 | def compile(self): 109 | """Compiles source code into a ROBLOX Model file.""" 110 | 111 | super().compile() 112 | self._write_model() 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir 2 | 3 | Compiles Lua source code into a ROBLOX-compatible XML file that can be easily 4 | imported into your game. 5 | 6 | Elixir is best for someone that prefers to work outside of ROBLOX Studio, 7 | where you can leverage the power of a version control system and your favorite 8 | text editor. 9 | 10 | ## Getting Started 11 | 12 | All you need is Python 3.4+ and you're good to go. You can install Elixir by 13 | simply running the setup script. 14 | 15 | ```bash 16 | $ python setup.py install 17 | ``` 18 | 19 | Elixir can be run in two ways. From the command line: 20 | 21 | ```bash 22 | # Displays all of the arguments and options that you need to know about. 23 | $ elixir -h 24 | ``` 25 | 26 | Or with a Python script: 27 | 28 | ```python 29 | # build.py 30 | from elixir.compilers import ModelCompiler 31 | 32 | source = "src/" 33 | dest = "model.rbxmx" 34 | 35 | compiler = ModelCompiler(source, dest) 36 | compiler.compile() 37 | ``` 38 | 39 | Running the `elixir` command lets you compile models quickly and easily, but if 40 | you have a lot of options you want to pass in, it can be beneficial to create a 41 | script like the above that you simply run with `python build.py`. 42 | 43 | ## Compilers 44 | 45 | Compilers are what take care of constructing the model file. They're the 46 | backbone of Elixir that brings everything together. Compilers and the command 47 | line are the primary interfaces to Elixir. 48 | 49 | ### elixir.compilers.ModelCompiler 50 | 51 | This is Elixir's primary compiler. It converts folders, Lua files, and ROBLOX 52 | models into a file that you can import into your game. 53 | 54 | This allows you to keep your codebase separate from your ROBLOX level. When 55 | you're ready to apply your code changes, you compile it into a model and drag it 56 | into your game. 57 | 58 | **Parameters:** 59 | 60 | - **_source_**: The directory containing Lua code and ROBLOX Models that you 61 | want compiled. 62 | 63 | - **_dest_**: The name of the file that will be created when compiling. 64 | 65 | Directories in this path are automatically created for you. For example, if 66 | you set this to `build/model.rbxmx`, the `build/` directory will be created if 67 | it doesn't already exist. 68 | 69 | It's important that the file extension should either be `.rbxmx` or `.rbxm`. 70 | Those are the two filetypes recognized by ROBLOX Studio. You won't be able to 71 | import the file into your game otherwise. 72 | 73 | - **_processor=BaseProcessor_**: The processor to use when compiling. A 74 | processor is what handles files and folders as the compiler comes across them. 75 | It dictates the type of ROBLOX class is returned. 76 | 77 | ## Properties 78 | 79 | When working with Elixir, there is no Properties panel like you would find in 80 | ROBLOX Studio. To make up for this, properties are defined using inline comments 81 | at the top of your Lua files. 82 | 83 | ```lua 84 | -- Name: HelloWorld 85 | -- ClassName: LocalScript 86 | 87 | local function hello() 88 | return "Hello, World!" 89 | end 90 | ``` 91 | 92 | When compiled, this would create a LocalScript named `HelloWorld`. 93 | 94 | List of properties: 95 | 96 | - **Name**: Any alphanumeric string can be used for the name (defaults to the 97 | file name). 98 | - **ClassName**: This can be any one of ROBLOX's Script instances (defaults to 99 | `Script`). 100 | - **Disabled**: Controls the Script's in-game `Disabled` property. You can set 101 | this to `true` or `false`. 102 | 103 | ## Modules 104 | 105 | Any Lua file with a `return` statement at the end of the file is assumed to be a 106 | ModuleScript when compiling. 107 | 108 | This means you generally won't have to bother with setting properties at the top 109 | of the file. One of the few cases where it's needed is when creating 110 | LocalScripts. 111 | 112 | ## Importing Models 113 | 114 | ROBLOX model files inside of the source directory will be automatically imported 115 | when compiling. 116 | 117 | **This only applies to files with the extension `rbxmx`.** That's the XML 118 | variant of ROBLOX's models. The other is `rbxm`, which is the binary format. 119 | Any model file in your source directory must be in XML if you want it to be 120 | imported. 121 | 122 | The contents of the model are unpacked to the current folder in-game when the 123 | compiler comes across them. The model file itself does not act as a folder. For 124 | example, if you had a project setup like this on your computer: 125 | 126 | ``` 127 | src/ 128 | Code/ 129 | AnotherScript.lua 130 | Script.lua 131 | Model.rbxmx 132 | ``` 133 | 134 | And the in-game contents of `Model.rbxmx` were: 135 | 136 | ![An in-game screenshot of the contents of Model.rbxmx](screenshots/example-model-contents.png) 137 | 138 | It would look like this in-game: 139 | 140 | ![An in-game screenshot of the compiled source code and Model.rbxmx](screenshots/example-compiled-source.png) 141 | 142 | ## Processors 143 | 144 | A processor is what the compilers use to determine what happens when they 145 | encounter a file or folder. They're easy to extend and allow you to process 146 | your source code in any way you want. 147 | 148 | You can make use of an existing processor with the `--processor` flag on the 149 | command line: 150 | 151 | ```bash 152 | $ elixir src/ model.rbxmx --processor NevermoreProcessor 153 | ``` 154 | 155 | Or by passing the processor into the compiler: 156 | 157 | ```python 158 | from elixir.compilerss import ModelCompiler 159 | from elixir.processors import NevermoreProcessor 160 | 161 | source = "src/" 162 | dest = "model.rbxmx" 163 | 164 | compiler = ModelCompiler(source, dest, processor=NevermoreProcessor) 165 | compiler.compile() 166 | ``` 167 | 168 | Be sure to read over everything a processor does so you don't get caught off 169 | guard. 170 | 171 | ### elixir.processors.BaseProcessor 172 | 173 | This is the default processor class that Elixir uses. All other processors 174 | should inherit from it. 175 | 176 | - All folders are compiled to ROBLOX `Folder` instances. 177 | - All Lua files are compiled to ROBLOX `Script` instances. 178 | - All ROBLOX XML models are unpacked at their position in the hierarchy. See 179 | [Importing Models](#importing-models) for more details. 180 | 181 | ### elixir.processors.NevermoreProcessor (Legacy) 182 | 183 | **NevermoreEngine no longer requires a custom processor.** This is kept for 184 | legacy support. The last version supported by this processor can be found 185 | [here](https://github.com/Quenty/NevermoreEngine/tree/b9b5a8). 186 | 187 | A processor for [NevermoreEngine](https://github.com/Quenty/NevermoreEngine), a 188 | project by Quenty to help you structure your game. 189 | 190 | - Overrides `model_name` to `Nevermore`. Nevermore internally references itself 191 | at `ServerScriptService.Nevermore`, so we need to ensure it's going by the 192 | correct name, otherwise it will error. 193 | - `NevermoreEngineLoader.lua` is compiled to a `Script`, and is the only one 194 | that will be enabled. 195 | - `Scripts` and `LocalScripts` will be disabled. 196 | - All other Lua files will be compiled into `ModuleScripts`. 197 | -------------------------------------------------------------------------------- /tests/test_rbxmx.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | from xml.etree import ElementTree 3 | 4 | from elixir import rbxmx 5 | 6 | """Tests for ROBLOX-related XML generation.""" 7 | 8 | SCRIPT_SOURCE = """\ 9 | -- Name: Hello 10 | -- ClassName: ModuleScript 11 | 12 | local module = {} 13 | 14 | function module.hello(name) 15 | name = name or "World" 16 | return "Hello" .. name .. "!" 17 | end 18 | 19 | return module 20 | """ 21 | 22 | def _new_item(class_name=None): 23 | """Helper function for creating new `Item` Elements. 24 | 25 | This is used until we get to InstanceElement, where we then use that class 26 | for all of the elements instead instead. 27 | """ 28 | class_name = class_name or "Folder" 29 | return ElementTree.Element("Item", attrib={ "class": class_name }) 30 | 31 | class TestBoolConversion: 32 | # I know the name sounds silly, but the function is used for putting bool 33 | # values into the XML, so it has to convert them into strings. 34 | def test_bool_is_string(self): 35 | assert type(rbxmx._convert_bool(True)) is str 36 | 37 | def test_bool_is_lowecased(self): 38 | assert rbxmx._convert_bool(True).islower() == True 39 | 40 | class TestSanitization: 41 | def test_is_converting_bools(self): 42 | assert type(rbxmx._sanitize(True)) is str 43 | 44 | def test_is_returning_same_content_if_not_sanitize(self): 45 | # As of writing this, strings do not get sanitized in any way. If this 46 | # changes in the future this test will fail. 47 | content = "Hello, World!" 48 | sanitized_content = rbxmx._sanitize(content) 49 | 50 | assert content == sanitized_content 51 | 52 | class TestModuleRecognition: 53 | content = "return value" 54 | 55 | def test_matches_module_with_excess_newlines(self): 56 | content = self.content + "\n\n\n\n" 57 | assert rbxmx.is_module(content) 58 | 59 | def test_matches_function_as_return_value(self): 60 | content = "return setmetatable(module, mt)" 61 | assert rbxmx.is_module(content) 62 | 63 | class TestBaseTag: 64 | def test_has_necessary_attributes(self): 65 | tag = rbxmx.get_base_tag() 66 | 67 | # Current this is the only attribute that's required for ROBLOX to 68 | # recognize the file as a Model. 69 | assert tag.get("version") 70 | 71 | class TestElementToStringConversion: 72 | def test_is_not_output_as_bytestring(self): 73 | item = _new_item() 74 | assert rbxmx.tostring(item) is not bytes 75 | 76 | def test_is_converting_to_string_properly(self): 77 | item = _new_item() 78 | expected_xml = "" 79 | assert rbxmx.tostring(item) == expected_xml 80 | 81 | class TestPropertyElement: 82 | def test_can_add_properties(self): 83 | item = _new_item() 84 | properties = rbxmx.PropertyElement(item) 85 | properties.add(tag="string", name="Property", text="Value") 86 | prop = properties.element.find("*[@name='Property']") 87 | 88 | assert prop.text == "Value" 89 | 90 | def test_can_get_properties_by_name(self): 91 | item = _new_item() 92 | properties = rbxmx.PropertyElement(item) 93 | properties.add(tag="string", name="Name", text="Property") 94 | 95 | prop = properties.get("Name") 96 | 97 | assert prop.text == "Property" 98 | 99 | def test_can_set_property_values(self): 100 | item = _new_item() 101 | properties = rbxmx.PropertyElement(item) 102 | properties.add(tag="string", name="Name", text="Property") 103 | prop = properties.get("Name") 104 | 105 | properties.set("Name", "Testing") 106 | 107 | assert prop.text == "Testing" 108 | 109 | class TestInstanceElement: 110 | instance = rbxmx.InstanceElement("Folder") 111 | element = instance.element 112 | 113 | def test_has_class_name(self): 114 | assert "class" in self.element.attrib 115 | 116 | def test_class_name_is_set_properly(self): 117 | assert self.element.get("class") == "Folder" 118 | 119 | def test_name_matches_class_name_by_default(self): 120 | class_name = self.element.get("class") 121 | name = self.instance.name.text 122 | assert class_name == name 123 | 124 | def test_has_properties(self): 125 | properties = self.element.find("Properties") 126 | assert properties 127 | 128 | def test_getting_the_internal_element_reference(self): 129 | element = self.instance.get_xml() 130 | assert element == self.instance.element 131 | 132 | def test_appending_to_other_elements(self): 133 | element = rbxmx.InstanceElement("Folder") 134 | another_element = rbxmx.InstanceElement("Model") 135 | 136 | xml = another_element.get_xml() 137 | 138 | element.append_to(xml) 139 | 140 | xml_was_appeneded = xml.find("Item") 141 | 142 | assert xml_was_appeneded 143 | 144 | class TestScriptElement: 145 | def test_disabled_is_converted_properly(self): 146 | script = rbxmx.ScriptElement("Script", disabled=True) 147 | assert script.disabled.text == "true" 148 | 149 | def test_source_can_be_blank(self): 150 | script = rbxmx.ScriptElement("Script", source=None) 151 | expected_xml = "" 152 | 153 | assert rbxmx.tostring(script.source) == expected_xml 154 | 155 | def test_can_have_source(self): 156 | script = rbxmx.ScriptElement("Script", 157 | source="print(\"Hello, World!\")") 158 | expected_xml = "print(\"Hello, " \ 159 | "World!\")" 160 | 161 | assert rbxmx.tostring(script.source) == expected_xml 162 | 163 | def test_can_have_varied_class_name(self): 164 | for script_class in [ "Script", "LocalScript", "ModuleScript" ]: 165 | script = rbxmx.ScriptElement(script_class) 166 | 167 | assert script.element.get("class") == script_class 168 | 169 | class TestScriptCommentMatching: 170 | def test_can_match_first_comment(self): 171 | comment = dedent("""\ 172 | -- Name: SomeScript 173 | -- ClassName: LocalScript 174 | 175 | print("Hello, World!")""") 176 | 177 | expected_output = dedent("""\ 178 | -- Name: SomeScript 179 | -- ClassName: LocalScript""") 180 | 181 | script = rbxmx.ScriptElement(source=comment) 182 | first_comment = script.get_first_comment() 183 | 184 | assert first_comment == expected_output 185 | 186 | def test_does_not_error_without_source(self): 187 | script = rbxmx.ScriptElement() # No `source` argument 188 | comment = script.get_first_comment() 189 | 190 | assert comment is None 191 | 192 | def test_does_not_error_without_first_comment(self): 193 | source = dedent("""\ 194 | local function hello() 195 | return "Hello, World!" 196 | end 197 | 198 | return hello""") 199 | 200 | script = rbxmx.ScriptElement(source=source) 201 | comment = script.get_first_comment() 202 | 203 | assert comment is None 204 | 205 | def test_does_not_match_block_comments(self): 206 | """ 207 | Right now, comment matching is only done to inline comments for 208 | simplicity. If a more sophisticated pattern is implemented to pick up 209 | block comments, this test can be removed. 210 | """ 211 | 212 | comment = dedent("""\ 213 | --[[ 214 | Hello, World! 215 | --]]""") 216 | 217 | script = rbxmx.ScriptElement(source=comment) 218 | first_comment = script.get_first_comment() 219 | 220 | assert first_comment is None 221 | 222 | class TestEmbeddedScriptProperties: 223 | def test_can_get_embedded_properties(self): 224 | source = dedent("""\ 225 | -- Name: HelloWorld 226 | -- ClassName: LocalScript 227 | 228 | print("Hello, World!")""") 229 | 230 | script = rbxmx.ScriptElement(source=source) 231 | properties = script.get_embedded_properties() 232 | 233 | assert properties["Name"] == "HelloWorld" 234 | assert properties["ClassName"] == "LocalScript" 235 | 236 | def test_can_use_only_one_embedded_property(self): 237 | source = "-- ClassName: LocalScript" 238 | 239 | script = rbxmx.ScriptElement(source=source) 240 | properties = script.get_embedded_properties() 241 | 242 | assert properties["ClassName"] == "LocalScript" 243 | 244 | def test_does_not_detect_regular_comments_as_embedded_properties(self): 245 | source = "-- This is a comment" 246 | 247 | script = rbxmx.ScriptElement(source=source) 248 | properties = script.get_embedded_properties() 249 | 250 | assert not properties 251 | 252 | def test_is_overriding_with_embedded_properties(self): 253 | source = dedent("""\ 254 | -- Name: HelloWorld 255 | -- ClassName: LocalScript 256 | 257 | print("Hello, World!")""") 258 | 259 | script = rbxmx.ScriptElement(name="SampleScript", source=source) 260 | 261 | assert script.name.text == "SampleScript" 262 | 263 | script.use_embedded_properties() 264 | 265 | assert script.name.text == "HelloWorld" 266 | 267 | def test_can_attempt_to_override_without_embedded_properties(self): 268 | source = dedent("""\ 269 | local function hello() 270 | return "Hello, World!" 271 | end 272 | 273 | return hello""") 274 | 275 | script = rbxmx.ScriptElement(source=source) 276 | script.use_embedded_properties() 277 | 278 | def test_fails_graceully_for_property_that_doesnt_exist(self): 279 | source = "-- NonExistentProperty: true" 280 | 281 | script = rbxmx.ScriptElement(source=source) 282 | script.use_embedded_properties() 283 | -------------------------------------------------------------------------------- /elixir/rbxmx.py: -------------------------------------------------------------------------------- 1 | import re 2 | from xml.etree import ElementTree 3 | 4 | """Everything to do with generating ROBLOX instances as XML. 5 | 6 | This module is for constructing ROBLOX Models from a folder structure and Lua 7 | files, as well as importing existing Models when compiling. 8 | """ 9 | 10 | def _is_lua_comment(line): 11 | """Checks if a line of text is a Lua comment. 12 | 13 | line : str 14 | A line from some Lua source code. 15 | """ 16 | 17 | # Matching spaces so that we don't pick up block comments (--[[ ]]) 18 | return re.match(r"^--\s+", line) 19 | 20 | def _convert_bool(b): 21 | """Converts Python bool values to Lua's. 22 | 23 | For simplicity's sake, you can use Python's bool values for everything in 24 | the codebase. Before it can go in the XML it has to be turned into a string, 25 | and it must also be lowercased to match Lua's bool values, otherwise ROBLOX 26 | won't recognize them. 27 | """ 28 | return str(b).lower() 29 | 30 | def _sanitize(content): 31 | """Makes sure `content` is safe to go in the XML. 32 | 33 | This is mostly for converting Python types into something XML compatible. 34 | """ 35 | 36 | if type(content) == bool: 37 | return _convert_bool(content) 38 | else: 39 | return content 40 | 41 | def get_base_tag(): 42 | """Gets the root tag. 43 | 44 | This is what makes ROBLOX recognize the file as a Model that it can import, 45 | it should always be the first Element in the file. All others are appended 46 | to this tag. 47 | """ 48 | 49 | # `version` is currently the only attribute that's required for ROBLOX to 50 | # recognize the file as a Model. All of the others are included to match 51 | # what ROBLOX outputs when exporting a model to your computer. 52 | return ElementTree.Element("roblox", attrib={ 53 | "xmlns:xmine": "http://www.w3.org/2005/05/xmlmime", 54 | "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", 55 | "xsi:noNamespaceSchemaLocation": "http://www.roblox.com/roblox.xsd", 56 | "version": "4" }) 57 | 58 | def is_module(content): 59 | """Checks if the contents are from a Lua module. 60 | 61 | It looks for a returned value at the end of the file. If it finds one, it's 62 | safe to assume that it's a module. 63 | 64 | content : str 65 | The Lua source code to check. 66 | """ 67 | 68 | # We match any number of whitespace after the `return` in case of accidental 69 | # spacing on the user's part. 70 | # 71 | # Then we match any characters to catch variables (`return module`) and 72 | # functions (`return setmetatable(t1, t2)`) 73 | # 74 | # We're optionally matching any number of spaces at the end of the file 75 | # incase of a final newline, or accidentally added spaces after the value. 76 | return re.search(r"return\s+.*(\s+)?$", content) 77 | 78 | def tostring(element): 79 | """A more specialized version of ElementTree's `tostring`. 80 | 81 | We're using Unicode encoding so that this returns a string, instead of the 82 | default bytestring. 83 | 84 | We're also setting `short_empty_elements` to `False` so that all elements 85 | are output with their ending tags. This is only required when _writing_ the 86 | XML, as ROBLOX won't import the file if there are any self-closing tags. 87 | 88 | This function isn't used for writing, so while ending tags aren't mandatory, 89 | they're used here so the string version of the XML is consistent with what 90 | gets written. 91 | """ 92 | 93 | return ElementTree.tostring(element, encoding="unicode", 94 | short_empty_elements=False) 95 | 96 | class PropertyElement: 97 | """Container for the properties of InstanceElement. 98 | 99 | This stores things like the Instance's in-game name, and for Scripts it can 100 | hold the source code, as well as if the Script is disabled or not. 101 | """ 102 | 103 | def __init__(self, parent): 104 | self.element = ElementTree.SubElement(parent, "Properties") 105 | 106 | def add(self, tag, name, text): 107 | """Add a new property. 108 | 109 | tag : str 110 | The type of property. This will typically be "string", "bool" or 111 | "ProtectedString". 112 | name : str 113 | The property's name. For example, you would use a tag of "bool" and 114 | a name of "Disabled" for the Disabled property of a Script. 115 | text : str|number|bool 116 | The contents of the property. For a Name or Source this will be a 117 | string, but you can also use Python's bool values with this, they 118 | just get converted to Lua's bools. 119 | """ 120 | 121 | prop = ElementTree.SubElement(self.element, tag, name=name) 122 | prop.text = _sanitize(text) 123 | 124 | return prop 125 | 126 | def get(self, name): 127 | """Gets a property by its name. 128 | 129 | name : str 130 | The name of the property to search for. This would be "Name" for an 131 | instance's name, "Source" for the Lua source code, etc. 132 | """ 133 | 134 | return self.element.find("*[@name='{}']".format(name)) 135 | 136 | def set(self, name, new_value): 137 | """Changes the contents of a property to a new value.""" 138 | 139 | prop = self.get(name) 140 | if prop is not None: 141 | prop.text = new_value 142 | 143 | class InstanceElement: 144 | """Acts as an XML implementation of ROBLOX's Instance class. 145 | 146 | This class is intended to be very barebones, you will typically only use it 147 | for Folder instances. Other classes like ScriptElement are created to extend 148 | its functionality. 149 | 150 | class_name : str 151 | This can be any ROBLOX class. 152 | http://wiki.roblox.com/index.php?title=API:Class_reference 153 | name=None : str 154 | The name of the instance in-game. This will default to `class_name`. 155 | """ 156 | 157 | def __init__(self, class_name, name=None): 158 | name = name or class_name 159 | 160 | # `class` is a reserved keyword so we have to pass it in through 161 | # `attrib` rather than as a named parameter. 162 | self.element = ElementTree.Element("Item", attrib={"class": class_name}) 163 | 164 | self.properties = PropertyElement(self.element) 165 | self.name = self.properties.add("string", "Name", name) 166 | 167 | def get_xml(self): 168 | return self.element 169 | 170 | def append_to(self, parent_element): 171 | """Appends the Element's XML to another Element. 172 | 173 | This is primarily used so that appending the XML of regular 174 | InstanceElement's and ModelElement's can be done from the same method. 175 | 176 | parent_element : Element 177 | The Element to append to. 178 | """ 179 | 180 | xml = self.get_xml() 181 | parent_element.append(xml) 182 | 183 | class ScriptElement(InstanceElement): 184 | def __init__(self, class_name="Script", name=None, source=None, 185 | disabled=False): 186 | super().__init__(class_name, name) 187 | 188 | self.source = self.properties.add("ProtectedString", "Source", source) 189 | self.disabled = self.properties.add("bool", "Disabled", disabled) 190 | 191 | def get_first_comment(self): 192 | """Gets the first comment in the source. 193 | 194 | This only applie to the first _inline_ comment (the ones started with 195 | two dashes), block comments are not picked up. 196 | 197 | This is used by get_embedded_properties() to parse the comment for 198 | properties to override the defaults. 199 | """ 200 | 201 | source = self.source.text 202 | 203 | if not source: return 204 | 205 | found_first_comment = False 206 | comment_lines = [] 207 | 208 | for line in source.splitlines(): 209 | is_comment = _is_lua_comment(line) 210 | if is_comment: 211 | found_first_comment = True 212 | comment_lines.append(line) 213 | 214 | # The first comment ended, time to break out 215 | elif not is_comment and found_first_comment: 216 | break 217 | 218 | if comment_lines: 219 | return "\n".join(comment_lines) 220 | 221 | def get_embedded_properties(self): 222 | """Gets properties that are embedded in the source. 223 | 224 | When working on the filesystem, there is no Properties panel like you 225 | would find in ROBLOX Studio. To make up for this, properties can be 226 | defined using inline comments at the top of your Lua files. 227 | 228 | If this instance had the following source: 229 | 230 | -- Name: HelloWorld 231 | -- ClassName: LocalScript 232 | 233 | local function hello() 234 | return "Hello, World!" 235 | end 236 | 237 | Running this method will return a dict of: 238 | 239 | { "Name": "HelloWorld", "ClassName": "LocalScript" } 240 | """ 241 | 242 | comment = self.get_first_comment() 243 | 244 | # If there's no comment then there won't be any embedded properties. No 245 | # need to continue from here. 246 | if not comment: return 247 | 248 | # For `name` we only need to match whole words, as ROBLOX's 249 | # properties don't have any special characters. `value` on the other 250 | # hand can use any character. 251 | property_pattern = re.compile(r"(?P\w+):\s+(?P.+)") 252 | 253 | property_list = {} 254 | 255 | for match in property_pattern.finditer(comment): 256 | property_list[match.group("name")] = match.group("value") 257 | 258 | return property_list 259 | 260 | def use_embedded_properties(self): 261 | """Overrides the current properties with any embedded ones.""" 262 | 263 | properties = self.get_embedded_properties() 264 | 265 | if not properties: return 266 | 267 | for prop_name, prop_value in properties.items(): 268 | if prop_name == "ClassName": 269 | self.element.set("class", prop_value) 270 | else: 271 | self.properties.set(prop_name, prop_value) 272 | 273 | class ContainerElement(InstanceElement): 274 | def __init__(self, class_name="Folder", name=None): 275 | super().__init__(class_name, name) 276 | 277 | class ModelElement(InstanceElement): 278 | def __init__(self, content): 279 | self.element = ElementTree.XML(content) 280 | 281 | def append_to(self, parent_element): 282 | """Imports the Model's contents into another Element. 283 | 284 | parent_element : Element 285 | The Element to append to. 286 | """ 287 | 288 | xml = self.get_xml() 289 | 290 | # Because Models have their own tag, when importing we have to 291 | # skip over that and just use the inner Elements. 292 | for element in list(xml): 293 | parent_element.append(element) 294 | -------------------------------------------------------------------------------- /examples/using-processors/src/Nevermore/App/NevermoreEngine.lua: -------------------------------------------------------------------------------- 1 | -- see readme.md 2 | -- @author Quenty 3 | 4 | --[[ 5 | Update August 16th, 2014 6 | - Any module with the name "Server" in it is not replicated, to prevent people from stealing 7 | nevermore's complete source. 8 | ]] 9 | 10 | local NevermoreEngine = {} 11 | 12 | local Players = game:GetService("Players") 13 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 14 | local MarketplaceService = game:GetService("MarketplaceService") 15 | local TestService = game:GetService("TestService") 16 | local StarterGui = game:GetService("StarterGui") 17 | local RunService = game:GetService("RunService") 18 | local ContentProvider = game:GetService("ContentProvider") 19 | local ServerStorage 20 | local ServerScriptService 21 | 22 | -- local RbxUtility = LoadLibrary("RbxUtility") 23 | 24 | local Configuration = { 25 | ClientName = "NevermoreClient"; 26 | ReplicatedPackageName = "NevermoreResources"; 27 | DataSteamName = "NevermoreDataStream"; 28 | NevermoreRequestPrefix = "NevermoreEngineRequest"; -- For network requests, what should it prefix it as? 29 | 30 | IsClient = script.Parent ~= nil; 31 | SoloTestMode = game:FindService("NetworkServer") == nil and game:FindService("NetworkClient") == nil; 32 | 33 | Blacklist = ""; -- Ban list 34 | CustomCharacters = false; -- When enabled, allows the client to set Player.Character itself. Untested! 35 | SplashScreen = true; -- Should a splashscreen be rendered? 36 | CharacterRespawnTime = 0.5; -- How long does it take for characters to respawn? Only kept updated on the server-side. 37 | ResetPlayerGuiOnSpawn = false; -- Set StarterGui.ResetPlayerGuiOnSpawn --- NOTE the client script must reparent itself to the PlayerGui 38 | } 39 | Configuration.IsServer = not Configuration.IsClient 40 | 41 | 42 | do 43 | -- Print headers are used to help debugging process 44 | if Configuration.SoloTestMode then 45 | Configuration.PrintHeader = "[NevermoreEngineSolo] - " 46 | else 47 | if Configuration.IsServer then 48 | Configuration.PrintHeader = "[NevermoreEngineServer] - " 49 | else 50 | Configuration.PrintHeader = "[NevermoreEngineLocal] - " 51 | end 52 | end 53 | 54 | local Output = Configuration.PrintHeader .. "NevermoreEngine loaded. Modes: " 55 | 56 | -- Print out information on whether or not FilteringEnabled is up or not. 57 | if workspace.FilteringEnabled then 58 | Output = Output .. " Filtering enabled. " 59 | end 60 | 61 | -- Print out whether or not ResetPlayerGuiOnSpawn is enabled or not. 62 | if not Configuration.ResetPlayerGuiOnSpawn or not StarterGui.ResetPlayerGuiOnSpawn then 63 | Output = Output .. " GUIs will not reload." 64 | StarterGui.ResetPlayerGuiOnSpawn = false 65 | end 66 | print(Output) 67 | end 68 | 69 | Players.CharacterAutoLoads = false 70 | 71 | -- If we're the server than we should retrieve server storage units. 72 | if not Configuration.IsClient then 73 | ServerStorage = game:GetService("ServerStorage") 74 | ServerScriptService = game:GetService("ServerScriptService") 75 | end 76 | 77 | ----------------------- 78 | -- UTILITY FUNCTIONS -- 79 | ----------------------- 80 | 81 | local function WaitForChild(Parent, Name) 82 | --- Yields until a child is added. Warns after 5 seconds of yield. 83 | -- @param Parent The parent to wait for the child of 84 | -- @param Name The name of the child 85 | -- @return The child found 86 | 87 | local Child = Parent:FindFirstChild(Name) 88 | local StartTime = tick() 89 | local Warned = false 90 | while not Child do 91 | wait(0) 92 | Child = Parent:FindFirstChild(Name) 93 | if not Warned and StartTime + 5 <= tick() then 94 | Warned = true; 95 | warn(Configuration.PrintHeader .. " " .. Name .. " has not replicated after 5 seconds, may not be able to execute Nevermore.") 96 | end 97 | end 98 | return Child 99 | end 100 | 101 | local function pack(...) 102 | --- Packs a tuple into a table and returns it 103 | -- @return The packed tuple 104 | 105 | return {...} 106 | end 107 | 108 | ------------------------------ 109 | -- Load Dependent Resources -- 110 | ------------------------------ 111 | local NevermoreContainer, ModulesContainer, ApplicationContainer, ReplicatedPackage, DataStreamContainer, EventStreamContainer 112 | do 113 | local function LoadResource(Parent, ResourceName) 114 | --- Loads a resource or errors. Makes sure that a resource is available. 115 | -- @param Parent The parent of the resource to load 116 | -- @param ResourceName The name of the resource attempting to load 117 | 118 | assert(type(ResourceName) == "string", "[NevermoreEngine] - ResourceName '" .. tostring(ResourceName) .. "' is " .. type(ResourceName) .. ", should be string") 119 | 120 | local Resource = Parent:FindFirstChild(ResourceName) 121 | if not Resource then 122 | error(Configuration.PrintHeader .. "Failed to load required resource '" .. ResourceName .. "', expected at '" .. Parent:GetFullName() .. "'", 2) 123 | return nil 124 | else 125 | return Resource 126 | end 127 | end 128 | 129 | if Configuration.IsServer then 130 | -- Load Resources -- 131 | 132 | NevermoreContainer = LoadResource(ServerScriptService, "Nevermore") 133 | ModulesContainer = LoadResource(NevermoreContainer, "Modules") 134 | ApplicationContainer = LoadResource(NevermoreContainer, "App") 135 | 136 | -- Create the replicated package -- 137 | ReplicatedPackage = ReplicatedStorage:FindFirstChild(Configuration.ReplicatedPackageName) 138 | if not ReplicatedPackage then 139 | ReplicatedPackage = Instance.new("Folder") 140 | ReplicatedPackage.Name = Configuration.ReplicatedPackageName 141 | ReplicatedPackage.Parent = ReplicatedStorage 142 | ReplicatedPackage.Archivable = false; 143 | end 144 | ReplicatedPackage:ClearAllChildren() 145 | 146 | DataStreamContainer = ReplicatedPackage:FindFirstChild("DataStreamContainer") 147 | if not DataStreamContainer then 148 | DataStreamContainer = Instance.new("Folder") 149 | DataStreamContainer.Name = "DataStreamContainer" 150 | DataStreamContainer.Parent = ReplicatedPackage 151 | DataStreamContainer.Archivable = false; 152 | end 153 | 154 | EventStreamContainer = ReplicatedPackage:FindFirstChild("EventStreamContainer") 155 | if not EventStreamContainer then 156 | EventStreamContainer = Instance.new("Folder") 157 | EventStreamContainer.Name = "EventStreamContainer" 158 | EventStreamContainer.Parent = ReplicatedPackage 159 | EventStreamContainer.Archivable = false; 160 | end 161 | 162 | DataStreamContainer:ClearAllChildren() 163 | else 164 | -- Handle replication for clients 165 | 166 | -- Load Resource Package -- 167 | ReplicatedPackage = WaitForChild(ReplicatedStorage, Configuration.ReplicatedPackageName) 168 | DataStreamContainer = WaitForChild(ReplicatedPackage, "DataStreamContainer") 169 | EventStreamContainer = WaitForChild(ReplicatedPackage, "EventStreamContainer") 170 | end 171 | end 172 | 173 | --print(Configuration.PrintHeader .. "Loaded dependent resources module") 174 | ------------------------ 175 | -- RESOURCE MANAGMENT -- 176 | ------------------------ 177 | local NetworkingRemoteFunction 178 | local ResouceManager = {} do 179 | --- Handles resource loading and replication 180 | local ResourceCache = {} 181 | local MainResourcesServer, MainResourcesClient 182 | 183 | if Configuration.IsServer then 184 | MainResourcesServer = {} -- Resources to load. 185 | MainResourcesClient = {} 186 | else 187 | MainResourcesClient = {} 188 | end 189 | 190 | local function GetDataStreamObject(Name, Parent) 191 | --- Products a new DataStream object if it doesn't already exist, otherwise 192 | -- return's the current datastream. 193 | -- @param Name The Name of the DataStream 194 | -- @param [Parent] The parent to add to 195 | 196 | Parent = Parent or DataStreamContainer 197 | 198 | local DataStreamObject = Parent:FindFirstChild(Name) 199 | if not DataStreamObject then 200 | if Configuration.IsServer then 201 | DataStreamObject = Instance.new("RemoteFunction") 202 | DataStreamObject.Name = Name; 203 | DataStreamObject.Archivable = false; 204 | DataStreamObject.Parent = Parent 205 | else 206 | DataStreamObject = WaitForChild(Parent, Name) -- Client side, we must wait.' 207 | end 208 | end 209 | return DataStreamObject 210 | end 211 | ResouceManager.GetDataStreamObject = GetDataStreamObject 212 | 213 | local function GetEventStreamObject(Name, Parent) 214 | --- Products a new EventStream object if it doesn't already exist, otherwise 215 | -- return's the current datastream. 216 | -- @param Name The Name of the EventStream 217 | -- @param [Parent] The parent to add to 218 | 219 | Parent = Parent or EventStreamContainer 220 | 221 | local DataStreamObject = Parent:FindFirstChild(Name) 222 | if not DataStreamObject then 223 | if Configuration.IsServer then 224 | DataStreamObject = Instance.new("RemoteEvent") 225 | DataStreamObject.Name = Name; 226 | DataStreamObject.Archivable = false; 227 | DataStreamObject.Parent = Parent 228 | else 229 | DataStreamObject = WaitForChild(Parent, Name) -- Client side, we must wait. 230 | end 231 | end 232 | return DataStreamObject 233 | end 234 | ResouceManager.GetEventStreamObject = GetEventStreamObject 235 | 236 | if Configuration.IsClient then 237 | NetworkingRemoteFunction = WaitForChild(DataStreamContainer, Configuration.DataSteamName) 238 | else 239 | NetworkingRemoteFunction = GetDataStreamObject(Configuration.DataSteamName, DataStreamContainer) 240 | end 241 | 242 | local function IsMainResource(Item) 243 | --- Finds out if an Item is considered a MainResource 244 | -- @return Boolean is a main resource 245 | 246 | if Item:IsA("Script") then 247 | if not Item.Disabled then 248 | -- If an item is not disabled, then it's disabled, but yell at 249 | -- the user. 250 | 251 | --if Item.Name:lower():match("\.main$") == nil then 252 | error(Configuration.PrintHeader .. Item:GetFullName() .. " is not disabled, and does not end with .Main.") 253 | --end 254 | 255 | return true 256 | end 257 | return Item.Name:lower():match("\.main$") ~= nil -- Check to see if it ends 258 | -- in .main, ignoring caps 259 | else 260 | return false; 261 | end 262 | end 263 | 264 | local function GetLoadablesForServer() 265 | --- Get's the loadable items for the server, that should be insta-ran 266 | -- @return A table full of the resources to be loaded 267 | 268 | return MainResourcesServer 269 | end 270 | ResouceManager.GetLoadablesForServer = GetLoadablesForServer 271 | 272 | local function GetLoadablesForClient() 273 | --- Get's the loadable items for the Client, that should be insta-ran 274 | -- @return A table full of the resources to be loaded 275 | 276 | return MainResourcesClient 277 | end 278 | ResouceManager.GetLoadablesForClient = GetLoadablesForClient 279 | 280 | local PopulateResourceCache 281 | if Configuration.IsClient then 282 | function PopulateResourceCache() 283 | --- Populates the resource cache. For the client. 284 | -- Should only be called once. Used internally. 285 | 286 | local Populate 287 | function Populate(Parent) 288 | for _, Item in pairs(Parent:GetChildren()) do 289 | if (Item:IsA("LocalScript") or Item:IsA("ModuleScript")) then 290 | 291 | ResourceCache[Item.Name] = Item; 292 | 293 | if IsMainResource(Item) then 294 | MainResourcesClient[#MainResourcesClient+1] = Item; 295 | end 296 | else 297 | Populate(Item) 298 | end 299 | end 300 | end 301 | 302 | Populate(ReplicatedPackage) 303 | end 304 | else -- Configuration.IsServer then 305 | function PopulateResourceCache() 306 | --- Populates the resource cache. For the server. Also populates 307 | -- the replication cache. Used internally. 308 | -- Should be called once. 309 | 310 | --[[local NevermoreModule = script:Clone() 311 | NevermoreModule.Archivable = false; 312 | NevermoreModule.Parent = ReplicatedStorage--]] 313 | 314 | local function Populate(Parent) 315 | for _, Item in pairs(Parent:GetChildren()) do 316 | if Item:IsA("Script") or Item:IsA("ModuleScript") then -- Will catch LocalScripts as they inherit from script 317 | if ResourceCache[Item.Name] then 318 | error(Configuration.PrintHeader .. "There are two Resources called '" .. Item:GetFullName() .."'. Nevermore failed to populate the cache..", 2) 319 | else 320 | if Item:IsA("LocalScript") or Item:IsA("ModuleScript") then 321 | -- Clone the item into the replication packet for 322 | -- replication. However, we do not clone server scripts. 323 | 324 | --[[local ItemClone 325 | 326 | if not (Item:IsA("ModuleScript") and Item.Name:lower():find("server")) then -- Don't clone scripts with the name "server" in it. 327 | ItemClone = Item:Clone() 328 | ItemClone.Archivable = false; 329 | ItemClone.Parent = ReplicatedPackage 330 | else 331 | print("Not cloning resource '" .. Item.Name .."' as it is server only.") 332 | end 333 | 334 | if Item:IsA("ModuleScript") then 335 | ResourceCache[Item.Name] = ItemClone or Item; 336 | elseif IsMainResource(Item) then 337 | MainResourcesClient[#MainResourcesClient+1] = Item 338 | end--]] 339 | 340 | if not (Item:IsA("ModuleScript") and Item.Name:lower():find("server")) then 341 | if not Configuration.SoloTestMode then -- Do not move parents in SoloTestMode (keeps the error monitoring correct). 342 | Item.Parent = ReplicatedPackage 343 | end 344 | end 345 | 346 | if Item:IsA("ModuleScript") then 347 | ResourceCache[Item.Name] = Item 348 | elseif IsMainResource(Item) then 349 | MainResourcesClient[#MainResourcesClient+1] = Item 350 | end 351 | 352 | else -- Do not replicate local scripts 353 | if IsMainResource(Item) then 354 | MainResourcesServer[#MainResourcesServer+1] = Item 355 | end 356 | ResourceCache[Item.Name] = Item 357 | end 358 | end 359 | else 360 | Populate(Item) 361 | --error(Configuration.PrintHeader .. "The resource '" .. Item:GetFullName() .."' is not a LocalScript, Script, or ModuleScript, and cannot be included. Nevermore failed to populate the cache..", 2) 362 | end 363 | end 364 | end 365 | 366 | Populate(ModulesContainer) 367 | end 368 | end 369 | ResouceManager.PopulateResourceCache = PopulateResourceCache 370 | 371 | local function GetResource(ResourceName) 372 | --- This script will load another script, module script, et cetera, if it is 373 | -- available. It will return the resource in question. 374 | -- @param ResourceName The name of the resource 375 | -- @return The found resource 376 | 377 | local ResourceFound = ResourceCache[ResourceName] 378 | 379 | if ResourceFound then 380 | return ResourceFound 381 | else 382 | error(Configuration.PrintHeader .. "The resource '" .. ResourceName .. "' does not exist, cannot load", 2) 383 | end 384 | end 385 | ResouceManager.GetResource = GetResource 386 | 387 | local function LoadScript(ScriptName) 388 | --- Runs a script, and can be called multiple times if the script is not 389 | -- a modular script. 390 | -- @param ScriptName The name of the script to load. 391 | 392 | local ScriptToLoad = GetResource(ScriptName) 393 | if ScriptToLoad and ScriptToLoad:IsA("Script") then 394 | local NewScript = ScriptToLoad:Clone() 395 | NewScript.Disabled = true; 396 | 397 | --[[if Configuration.SoloTestMode then 398 | if NewScript:IsA("LocalScript") then 399 | NewScript.Parent = Players.LocalPlayer:FindFirstChild("PlayerGui") 400 | else 401 | NewScript.Parent = script; 402 | end 403 | else 404 | NewScript.Parent = script; 405 | end--]] 406 | if Configuration.IsServer then 407 | NewScript.Parent = NevermoreContainer; 408 | else 409 | NewScript.Parent = Players.LocalPlayer:FindFirstChild("PlayerGui") 410 | end 411 | 412 | spawn(function() 413 | wait(0) 414 | NewScript.Disabled = false; 415 | end) 416 | else 417 | error(Configuration.PrintHeader .. "The script '" .. ScriptName .. "' is a '".. (ScriptToLoad and ScriptToLoad.ClassName or "nil value") .. "' and cannot be loaded", 2) 418 | end 419 | end 420 | ResouceManager.LoadScript = LoadScript 421 | 422 | if Configuration.IsServer then 423 | local function LoadScriptOnClient(Script, Client) 424 | --- Runs a script on the client. Used internally. 425 | -- @param Script The script to load. Should be a script object 426 | -- @param Client The client to run the script on. Should be a Player 427 | -- object 428 | 429 | if Script and Script:IsA("LocalScript") then 430 | local NewScript = Script:Clone() 431 | NewScript.Disabled = true; 432 | NewScript.Parent = Client:FindFirstChild("PlayerGui") 433 | 434 | spawn(function() 435 | wait(0) 436 | NewScript.Disabled = false; 437 | end) 438 | else 439 | error(Configuration.PrintHeader .. "The script '" .. tostring(Script) .. "' is a '" .. (Script and Script.ClassName or "nil value") .. "' and cannot be loaded", 2) 440 | end 441 | end 442 | ResouceManager.LoadScriptOnClient = LoadScriptOnClient 443 | 444 | local function ExecuteExecutables() 445 | --- Executes all the executable scripts on the server. 446 | 447 | for _, Item in pairs(GetLoadablesForServer()) do 448 | LoadScript(Item.Name) 449 | end 450 | end 451 | ResouceManager.ExecuteExecutables = ExecuteExecutables 452 | end 453 | 454 | --[[local NativeImports 455 | 456 | local function ImportLibrary(LibraryDefinition, Environment, Prefix) 457 | --- Imports a library into a given environment, potentially adding a PreFix 458 | -- into any of the values of the library, 459 | -- incase that's wanted. :) 460 | -- @param LibraryDefinition Table, the libraries definition 461 | -- @param Environment Another table, probably received by getfenv() in Lua 5.1, and __ENV in Lua 5.2 462 | -- @Param [Prefix] Optional string that will be prefixed to each function imported into the environment. 463 | 464 | if type(LibraryDefinition) ~= "table" then 465 | error(Configuration.PrintHeader .. "The LibraryDefinition argument must be a table, got '" .. tostring(LibraryDefinition) .. "'", 2) 466 | elseif type(Environment) ~= "table" then 467 | error(Configuration.PrintHeader .. "The Environment argument must be a table, got '" .. tostring(Environment) .. "'", 2) 468 | else 469 | Prefix = Prefix or ""; 470 | 471 | for Name, Value in pairs(LibraryDefinition) do 472 | if Environment[Prefix .. Name] == nil and not NativeImports[Name] then 473 | Environment[Prefix .. Name] = LibraryDefinition[Name] 474 | elseif not NativeImports[Name] then 475 | error(Configuration.PrintHeader .. "Failed to import function '" .. (Prefix .. Name) .. "' as it already exists in the environment", 2) 476 | end 477 | end 478 | end 479 | end 480 | ResouceManager.ImportLibrary = ImportLibrary 481 | 482 | -- List of functions to import into each library. In this case, only the 483 | -- environmental import functions and added to each library. 484 | NativeImports = { 485 | import = ImportLibrary; 486 | Import = ImportLibrary; 487 | }--]] 488 | 489 | local function LoadLibrary(LibraryName) 490 | --- Load's a modular script and packages it as a library. 491 | -- @param LibraryName A string of the resource that ist the LibraryName 492 | 493 | -- print(Configuration.PrintHeader .. "Loading Library " .. LibraryName) 494 | 495 | local ModularScript = GetResource(LibraryName) 496 | 497 | if ModularScript then 498 | if ModularScript:IsA("ModuleScript") then 499 | -- print(Configuration.PrintHeader .. "Loading Library " .. ModularScript:GetFullName()) 500 | local LibraryDefinition = require(ModularScript) 501 | 502 | --[[if type(LibraryDefinition) == "table" then 503 | -- Import native definitions 504 | for Name, Value in pairs(NativeImports) do 505 | if LibraryDefinition[Name] == nil then 506 | LibraryDefinition[Name] = Value 507 | end 508 | end 509 | --else 510 | --error(Configuration.PrintHeader .. " Library '" .. LibraryName .. "' did not return a table, returned a '" .. type(LibraryDefinition) .. "' value, '" .. tostring(LibraryDefinition) .. "'") 511 | end--]] 512 | 513 | return LibraryDefinition 514 | else 515 | error(Configuration.PrintHeader .. " The resource " .. LibraryName 516 | .. " is not a ModularScript, as expected, it is a " 517 | .. ModularScript.ClassName, 2 518 | ) 519 | end 520 | else 521 | error(Configuration.PrintHeader .. " Could not identify a library known as '" .. LibraryName .. "'", 2) 522 | end 523 | end 524 | ResouceManager.LoadLibrary = LoadLibrary 525 | end 526 | 527 | --print(Configuration.PrintHeader .. "Loaded resource manager module") 528 | 529 | ----------------------------- 530 | -- NETWORKING STREAM SETUP -- 531 | ----------------------------- 532 | local Network = {} -- API goes in here 533 | --[[ 534 | Contains the following API: 535 | 536 | Network.GetDataStream 537 | Network.GetDataStream 538 | 539 | --]] 540 | do 541 | --- Handles networking and PlayerLoading 542 | local DataStreamMain 543 | local GetDataStream 544 | local DataStreamCache = {} 545 | -- setmetatable(DataStreamCache, {__mode = "v"}); 546 | 547 | local function GetCachedDataStream(RemoteFunction) 548 | --- Creates a datastream filter that will take requests and 549 | -- filter them out. 550 | -- @param RemoteFunction A remote function to connect to 551 | 552 | -- Execute on the server: 553 | --- Execute ( Player Client , [...] ) 554 | -- Execute on the client: 555 | --- Execute ( [...] ) 556 | if DataStreamCache[RemoteFunction] then 557 | if Configuration.IsClient then 558 | DataStreamCache[RemoteFunction].ReloadConnection() 559 | end 560 | return DataStreamCache[RemoteFunction] 561 | else 562 | local DataStream = {} 563 | local RequestTagDatabase = {} 564 | 565 | -- Set request handling, for solo test mode. The problem here is that Server and Client scripts share the same 566 | -- code base, because both load the same engine in replicated storage. 567 | local function Send(...) 568 | -- print(Configuration.PrintHeader .. " Sending SoloTestMode") 569 | -- print(...) 570 | 571 | local Arguments = {...} 572 | local PossibleClient = Arguments[1] 573 | if PossibleClient and type(PossibleClient) == "userdata" and PossibleClient:IsA("Player") then 574 | local Request = Arguments[2] 575 | if type(Request) == "string" then 576 | local OtherArguments = {} 577 | for Index=3, #Arguments do 578 | OtherArguments[#OtherArguments+1] = Arguments[Index] 579 | end 580 | 581 | return RemoteFunction:InvokeClient(PossibleClient, Request:lower(), unpack(OtherArguments)) 582 | else 583 | error(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(Request) .. "' received. Resolved into '" .. tostring(Request) .. "'") 584 | return nil 585 | end 586 | elseif type(PossibleClient) == "string" then 587 | local Request = PossibleClient 588 | 589 | if type(Request) == "string" then 590 | local OtherArguments = {} 591 | for Index=2, #Arguments do 592 | OtherArguments[#OtherArguments+1] = Arguments[Index] 593 | end 594 | 595 | -- print("Invoke server") 596 | return RemoteFunction:InvokeServer(Request:lower(), unpack(OtherArguments)) 597 | else 598 | error(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(Request) .. "' received. Resolved into '" .. tostring(Request) .. "'") 599 | return nil 600 | end 601 | else 602 | error(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(PossibleClient)) 603 | end 604 | end 605 | 606 | local function SpawnSend(...) 607 | --- Sends the data, but doesn't wait for a response or return one. 608 | 609 | local Data = {...} 610 | spawn(function() 611 | Send(unpack(Data)) 612 | end) 613 | end 614 | 615 | if Configuration.IsServer or Configuration.SoloTestMode then 616 | function RemoteFunction.OnServerInvoke(Client, Request, ...) 617 | --- Handles incoming requests 618 | -- @param Client The client the request is being sent to 619 | -- @param Request The request string that is being sent 620 | -- @param [...] The extra parameters of the request 621 | -- @return The results, if successfully executed 622 | 623 | if type(Request) == "string" then 624 | -- print(Configuration.PrintHeader .. "Server request received") 625 | -- print(...) 626 | 627 | if Client == nil then 628 | if Configuration.SoloTestMode then 629 | Client = Players.LocalPlayer 630 | else 631 | error(Configuration.PrintHeader .. "No client provided") 632 | end 633 | end 634 | 635 | local RequestExecuter = RequestTagDatabase[Request:lower()] 636 | local RequestArguments = {...} 637 | if RequestExecuter then 638 | --[[local Results 639 | Results = pack(RequestExecuter(Client, unpack(RequestArguments))) 640 | return unpack(Results)--]] 641 | 642 | return RequestExecuter(Client, unpack(RequestArguments)) 643 | else 644 | warn(Configuration.PrintHeader .. "Unregistered request called, request tag '" .. Request .. "'.") 645 | return nil 646 | end 647 | else 648 | warn(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(Request) .. "' received. Resolved into '" .. tostring(Request) .. "'") 649 | return nil 650 | end 651 | end 652 | 653 | if not Configuration.SoloTestMode then 654 | function Send(Client, Request, ...) 655 | --- Sends a request to the client 656 | -- @param Client Player object, the client to send the request too 657 | -- @param Request the request to send it too. 658 | -- @return The results / derived data from the feedback 659 | 660 | -- DEBUG -- 661 | --print(Configuration.PrintHeader .. " Sending Request '" .. Request .. "' to Client '" .. tostring(Client) .. "'.") 662 | 663 | return RemoteFunction:InvokeClient(Client, Request:lower(), ...) 664 | end 665 | end 666 | end 667 | if Configuration.IsClient or Configuration.SoloTestMode then -- Handle clientside streaming. 668 | -- We do this for solotest mode, to connect the OnClientInvoke and the OnServerInvoke 669 | 670 | 671 | function DataStream.ReloadConnection() 672 | --- Reloads the OnClientInvoke event, which gets disconnected when scripts die on the client. 673 | -- However, this fixes it, because those scripts have to request the events every time. 674 | 675 | --[[ 676 | -- Note: When using RemoteFunctions, in a module script, on ROBLOX, and you load the ModuleScript 677 | with a LOCAL SCRIPT. When this LOCAL SCRIPT is killed, your OnClientInvoke function will be GARBAGE 678 | COLLECTED. You must thus, reload the OnClientInvoke function everytime the local script is loaded. 679 | 680 | --]] 681 | 682 | function RemoteFunction.OnClientInvoke(Request, ...) 683 | --- Handles incoming requests 684 | -- @param Request The request string that is being sent 685 | -- @param [...] The extra parameters of the request 686 | -- @return The results, if successfully executed 687 | 688 | 689 | if type(Request) == "string" then 690 | -- print(Configuration.PrintHeader .. "Client request received") 691 | -- print(...) 692 | 693 | local RequestExecuter = RequestTagDatabase[Request:lower()] 694 | local RequestArguments = {...} 695 | if RequestExecuter then 696 | spawn(function() 697 | RequestExecuter(unpack(RequestArguments)) 698 | end) 699 | else 700 | -- warn(Configuration.PrintHeader .. "Unregistered request called, request tag '" .. Request .. "'.") 701 | end 702 | else 703 | error(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(Request) .. "' received. Resolved into '" .. tostring(Request) .. "'") 704 | end 705 | end 706 | end 707 | 708 | --- Reload the initial connection. 709 | DataStream.ReloadConnection() 710 | 711 | if not Configuration.SoloTestMode then 712 | function Send(Request, ...) 713 | --- Sends a request to the server 714 | -- @param Request the request to send it too. 715 | -- @return The results / derived data from the feedback 716 | 717 | return RemoteFunction:InvokeServer(Request:lower(), ...) 718 | end 719 | end 720 | end 721 | DataStream.Send = Send 722 | DataStream.send = Send 723 | DataStream.Call = Send 724 | DataStream.call = Send 725 | DataStream.SpawnSend = SpawnSend 726 | DataStream.spawnSend = SpawnSend 727 | DataStream.spawn_send = SpawnSend 728 | 729 | local function RegisterRequestTag(RequestTag, Execute) 730 | --- Registers a request when sent 731 | -- @param RequestTag The tag that is expected 732 | -- @param Execute The functon to execute. It will be sent 733 | -- all remainig arguments. 734 | -- Request tags are not case sensitive 735 | 736 | --if not RequestTagDatabase[RequestTag:lower()] then 737 | RequestTagDatabase[RequestTag:lower()] = Execute; 738 | --else 739 | --error(Configuration.PrintHeader .. "The request tag " .. RequestTag:lower() .. " is already registered.") 740 | --end 741 | end 742 | DataStream.RegisterRequestTag = RegisterRequestTag 743 | DataStream.registerRequestTag = RegisterRequestTag 744 | 745 | local function UnregisterRequestTag(RequestTag) 746 | --- Unregisters the request from the tag 747 | -- @param RequestTag String the tag to reregister 748 | RequestTagDatabase[RequestTag:lower()] = nil; 749 | end 750 | DataStream.UnregisterRequestTag = UnregisterRequestTag 751 | DataStream.unregisterRequestTag = UnregisterRequestTag 752 | 753 | DataStreamCache[RemoteFunction] = DataStream 754 | return DataStream 755 | end 756 | end 757 | 758 | local function GetCachedEventStream(RemoteEvent) 759 | -- Like GetCachedDataStream, but with RemoteEvents 760 | -- @param RemoteEvent The remote event to get the stream for. 761 | 762 | if DataStreamCache[RemoteEvent] then 763 | if Configuration.IsClient then 764 | DataStreamCache[RemoteEvent].ReloadConnection() 765 | end 766 | return DataStreamCache[RemoteEvent] 767 | else 768 | local DataStream = {} 769 | local RequestTagDatabase = {} 770 | 771 | local function Fire(...) 772 | local Arguments = {...} 773 | local PossibleClient = Arguments[1] 774 | if PossibleClient and type(PossibleClient) == "userdata" and PossibleClient:IsA("Player") then 775 | local Request = Arguments[2] 776 | if type(Request) == "string" then 777 | local OtherArguments = {} 778 | for Index=3, #Arguments do 779 | OtherArguments[#OtherArguments+1] = Arguments[Index] 780 | end 781 | 782 | return RemoteEvent:FireClient(PossibleClient, Request:lower(), unpack(OtherArguments)) 783 | else 784 | error(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(Request) .. "' received. Resolved into '" .. tostring(Request) .. "'") 785 | return nil 786 | end 787 | elseif type(PossibleClient) == "string" then 788 | local Request = PossibleClient 789 | 790 | if type(Request) == "string" then 791 | local OtherArguments = {} 792 | for Index=2, #Arguments do 793 | OtherArguments[#OtherArguments+1] = Arguments[Index] 794 | end 795 | 796 | return RemoteEvent:FireServer(Request:lower(), unpack(OtherArguments)) 797 | else 798 | error(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(Request) .. "' received. Resolved into '" .. tostring(Request) .. "'") 799 | return nil 800 | end 801 | else 802 | error(Configuration.PrintHeader .. "Invalid request DataType to the DataStream, DataType '" .. type(PossibleClient) .. "', String expected.") 803 | end 804 | end 805 | 806 | local function FireAllClients(Request, ...) 807 | if type(Request) == "string" then 808 | RemoteEvent:FireAllClients(Request, ...) 809 | else 810 | error(Configuration.PrintHeader .. "Invalid reques DataType to the DataStream, DataType '" .. type(Request)) 811 | end 812 | end 813 | 814 | if Configuration.IsServer or Configuration.SoloTestMode then 815 | RemoteEvent.OnServerEvent:connect(function(Client, Request, ...) 816 | --- Handles incoming requests 817 | -- @param Client The client the request is being sent to 818 | -- @param Request The request string that is being sent 819 | -- @param [...] The extra parameters of the request 820 | -- @return The results, if successfully executed 821 | 822 | if type(Request) == "string" then 823 | if Client == nil then 824 | if Configuration.SoloTestMode then 825 | Client = Players.LocalPlayer 826 | else 827 | error(Configuration.PrintHeader .. "No client provided") 828 | end 829 | end 830 | 831 | local RequestExecuter = RequestTagDatabase[Request:lower()] 832 | local RequestArguments = {...} 833 | if RequestExecuter then 834 | RequestExecuter(Client, unpack(RequestArguments)) 835 | else 836 | -- warn(Configuration.PrintHeader .. "Unregistered request called, request tag '" .. Request .. "'.") 837 | end 838 | else 839 | error(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(Request) .. "' received. Resolved into '" .. tostring(Request) .. "'") 840 | end 841 | end) 842 | 843 | if not Configuration.SoloTestMode then 844 | function Fire(Client, Request, ...) 845 | --- Sends a request to the client 846 | -- @param Client Player object, the client to send the request too 847 | -- @param Request the request to send it too. 848 | -- @return The results / derived data from the feedback 849 | 850 | RemoteEvent:FireClient(Client, Request:lower(), ...) 851 | end 852 | end 853 | end 854 | if Configuration.IsClient or Configuration.SoloTestMode then -- Handle clientside streaming. 855 | -- We do this for solotest mode, to connect the OnClientInvoke and the OnServerInvoke 856 | 857 | local Event 858 | function DataStream.ReloadConnection() 859 | --- Reloads the OnClientInvoke event, which gets disconnected when scripts die on the client. 860 | -- However, this fixes it, because those scripts have to request the events every time. 861 | 862 | if Event then 863 | Event:disconnect() 864 | end 865 | 866 | Event = RemoteEvent.OnClientEvent:connect(function(Request, ...) 867 | --- Handles incoming requests 868 | -- @param Request The request string that is being sent 869 | -- @param [...] The extra parameters of the request 870 | -- @return The results, if successfully executed 871 | 872 | if type(Request) == "string" then 873 | local RequestExecuter = RequestTagDatabase[Request:lower()] 874 | local RequestArguments = {...} 875 | if RequestExecuter then 876 | spawn(function() 877 | RequestExecuter(unpack(RequestArguments)) 878 | end) 879 | else 880 | -- warn(Configuration.PrintHeader .. "Unregistered request called, request tag '" .. Request .. "'.") 881 | end 882 | else 883 | error(Configuration.PrintHeader .. "Invalid request to the DataStream, DataType '" .. type(Request) .. "' received. Resolved into '" .. tostring(Request) .. "'") 884 | end 885 | end) 886 | end 887 | 888 | --- Reload the initial connection. 889 | DataStream.ReloadConnection() 890 | 891 | if not Configuration.SoloTestMode then 892 | function Fire(Request, ...) 893 | --- Sends a request to the server 894 | -- @param Request the request to send it too. 895 | -- @return The results / derived data from the feedback 896 | 897 | RemoteEvent:FireServer(Request:lower(), ...) 898 | end 899 | end 900 | end 901 | DataStream.Fire = Fire 902 | DataStream.fire = Fire 903 | DataStream.FireAllClients = FireAllClients 904 | DataStream.fireAllClients = FireAllClients 905 | 906 | local function RegisterRequestTag(RequestTag, Execute) 907 | --- Registers a request when sent 908 | -- @param RequestTag The tag that is expected 909 | -- @param Execute The functon to execute. It will be sent 910 | -- all remainig arguments. 911 | -- Request tags are not case sensitive 912 | 913 | RequestTagDatabase[RequestTag:lower()] = Execute; 914 | end 915 | DataStream.RegisterRequestTag = RegisterRequestTag 916 | DataStream.registerRequestTag = RegisterRequestTag 917 | 918 | local function UnregisterRequestTag(RequestTag) 919 | --- Unregisters the request from the tag 920 | -- @param RequestTag String the tag to reregister 921 | 922 | RequestTagDatabase[RequestTag:lower()] = nil; 923 | end 924 | DataStream.UnregisterRequestTag = UnregisterRequestTag 925 | DataStream.unregisterRequestTag = UnregisterRequestTag 926 | 927 | DataStreamCache[RemoteEvent] = DataStream 928 | return DataStream 929 | end 930 | end 931 | 932 | local DataStreamMain = GetCachedDataStream(NetworkingRemoteFunction) 933 | 934 | local function GetDataStream(DataStreamName) 935 | --- Get's a dataStream channel 936 | -- @param DataSteamName The channel to log in to. 937 | -- @return The main datastream, if no DataSteamName is provided 938 | 939 | if DataStreamName then 940 | return GetCachedDataStream(ResouceManager.GetDataStreamObject(DataStreamName, DataStreamContainer)) 941 | else 942 | error("[NevermoreEngine] - Paramter DataStreamName was nil") 943 | end 944 | end 945 | Network.GetDataStream = GetDataStream 946 | 947 | local function GetEventStream(EventStreamName) 948 | --- Get's an EventStream chanel 949 | -- @param DataSteamName The channel to log in to. 950 | -- @return The main datastream, if no DataSteamName is provided 951 | 952 | if EventStreamName then 953 | return GetCachedEventStream(ResouceManager.GetEventStreamObject(EventStreamName, EventStreamContainer)) 954 | else 955 | error("[NevermoreEngine] - Paramter EventStreamName was nil") 956 | end 957 | end 958 | Network.GetEventStream = GetEventStream 959 | 960 | local function GetMainDatastream() 961 | --- Return's the main datastream, used internally for networking 962 | 963 | return DataStreamMain 964 | end 965 | Network.GetMainDatastream = GetMainDatastream 966 | 967 | local SplashConfiguration = { 968 | BackgroundColor3 = Color3.new(237/255, 236/255, 233/255); -- Color of background of loading screen. 969 | AccentColor3 = Color3.new(8/255, 130/255, 83/255); -- Not used. 970 | LogoSize = 200; 971 | LogoTexture = "http://www.roblox.com/asset/?id=129733987"; 972 | LogoSpacingUp = 70; -- Pixels up from loading frame. 973 | ParticalOrbitDistance = 50; -- How far out the particals orbit 974 | ZIndex = 10; 975 | } 976 | 977 | 978 | local function GenerateInitialSplashScreen(Player) 979 | --- Generates the initial SplashScreen for the player. 980 | -- @param Player The player to genearte the SplashScreen in. 981 | -- @return The generated splashsreen 982 | 983 | 984 | local ScreenGui = Instance.new("ScreenGui", Player:FindFirstChild("PlayerGui")) 985 | ScreenGui.Name = "SplashScreen"; 986 | 987 | local MainFrame = Instance.new('Frame') 988 | MainFrame.Name = "SplashScreen"; 989 | MainFrame.Position = UDim2.new(0, 0, 0, -38); -- top bar fix 990 | MainFrame.Size = UDim2.new(1, 0, 1, 60); -- Sized ans positioned weirdly because ROBLOX's ScreenGui doesn't cover the whole screen. 991 | MainFrame.BackgroundColor3 = SplashConfiguration.BackgroundColor3; 992 | MainFrame.Visible = true; 993 | MainFrame.ZIndex = SplashConfiguration.ZIndex; 994 | MainFrame.BorderSizePixel = 0; 995 | MainFrame.Parent = ScreenGui; 996 | 997 | local ParticalFrame = Instance.new('Frame') 998 | ParticalFrame.Name = "ParticalFrame"; 999 | ParticalFrame.Position = UDim2.new(0.5, -SplashConfiguration.ParticalOrbitDistance, 0.7, -SplashConfiguration.ParticalOrbitDistance); 1000 | ParticalFrame.Size = UDim2.new(0, SplashConfiguration.ParticalOrbitDistance*2, 0, SplashConfiguration.ParticalOrbitDistance*2); -- Sized ans positioned weirdly because ROBLOX's ScreenGui doesn't cover the whole screen. 1001 | ParticalFrame.Visible = true; 1002 | ParticalFrame.BackgroundTransparency = 1 1003 | ParticalFrame.ZIndex = SplashConfiguration.ZIndex; 1004 | ParticalFrame.BorderSizePixel = 0; 1005 | ParticalFrame.Parent = MainFrame; 1006 | 1007 | local LogoLabel = Instance.new('ImageLabel') 1008 | LogoLabel.Name = "LogoLabel"; 1009 | LogoLabel.Position = UDim2.new(0.5, -SplashConfiguration.LogoSize/2, 0.7, -SplashConfiguration.LogoSize/2 - SplashConfiguration.ParticalOrbitDistance*2 - SplashConfiguration.LogoSpacingUp); 1010 | LogoLabel.Size = UDim2.new(0, SplashConfiguration.LogoSize, 0, SplashConfiguration.LogoSize); -- Sized ans positioned weirdly because ROBLOX's ScreenGui doesn't cover the whole screen. 1011 | LogoLabel.Visible = true; 1012 | LogoLabel.BackgroundTransparency = 1 1013 | LogoLabel.Image = SplashConfiguration.LogoTexture; 1014 | LogoLabel.ZIndex = SplashConfiguration.ZIndex; 1015 | LogoLabel.BorderSizePixel = 0; 1016 | LogoLabel.Parent = MainFrame; 1017 | LogoLabel.ImageTransparency = 1; 1018 | 1019 | --[[ 1020 | local LoadingText = Instance.new("TextLabel") 1021 | LoadingText.Name = "LoadingText" 1022 | LoadingText.Position = UDim2.new(0.5, -SplashConfiguration.LogoSize/2, 0.7, -SplashConfiguration.LogoSize/2 - SplashConfiguration.ParticalOrbitDistance*2 - SplashConfiguration.LogoSpacingUp); 1023 | LoadingText.Size = UDim2.new(0, SplashConfiguration.LogoSize, 0, SplashConfiguration.LogoSize); -- Sized ans positioned weirdly because ROBLOX's ScreenGui doesn't cover the whole screen. 1024 | LoadingText.Visible = true; 1025 | LoadingText.BackgroundTransparency = 1 1026 | LoadingText.ZIndex = SplashConfiguration.ZIndex; 1027 | LoadingText.BorderSizePixel = 0; 1028 | LoadingText.TextColor3 = SplashConfiguration.AccentColor3; 1029 | LoadingText.TextXAlignment = "Center"; 1030 | LoadingText.Text = "Boostrapping Adventure..." 1031 | LoadingText.Parent = MainFrame; 1032 | --]] 1033 | 1034 | return ScreenGui 1035 | end 1036 | 1037 | if Configuration.IsServer then 1038 | local function CheckIfPlayerIsBlacklisted(Player, BlackList) 1039 | --- Checks to see if a player is blacklisted from the server 1040 | -- @param Player The player to check for 1041 | -- @param Blacklist The string blacklist 1042 | -- @return Boolean is blacklisted 1043 | 1044 | for Id in string.gmatch(BlackList, "%d+") do 1045 | local ProductInformation = (MarketplaceService:GetProductInfo(Id)) 1046 | if ProductInformation then 1047 | if string.match(ProductInformation["Description"], Player.Name..";") then 1048 | return true; 1049 | end 1050 | end 1051 | end 1052 | for Name in string.gmatch(BlackList, "[%a%d]+") do 1053 | if Player.Name:lower() == Name:lower() then 1054 | return true; 1055 | end 1056 | end 1057 | return false; 1058 | end 1059 | 1060 | local function CheckPlayer(player) 1061 | --- Makes sure a player has all necessary components. 1062 | -- @return Boolean If the player has all the right components 1063 | 1064 | return player and player:IsA("Player") 1065 | and player:FindFirstChild("Backpack") 1066 | and player:FindFirstChild("StarterGear") 1067 | -- and player.PlayerGui:IsA("PlayerGui") -- PlayerGui does not replicate to other clients. 1068 | end 1069 | 1070 | local function CheckCharacter(player) 1071 | --- Make sure that a character has all necessary components 1072 | -- @return Boolean If the player has all the right components 1073 | 1074 | local character = player.Character; 1075 | return character 1076 | and character:FindFirstChild("Humanoid") 1077 | and character:FindFirstChild("Torso") 1078 | and character:FindFirstChild("Head") 1079 | and character.Humanoid:IsA("Humanoid") 1080 | and character.Head:IsA("BasePart") 1081 | and character.Torso:IsA("BasePart") 1082 | end 1083 | 1084 | 1085 | local function DumpExecutables(Player) 1086 | --- Executes all "MainResources" for the player 1087 | -- @param Player The player to load the resources on 1088 | 1089 | if Player:IsDescendantOf(Players) then 1090 | print(Configuration.PrintHeader .. "Loading executables onto " .. tostring(Player)) 1091 | for _, Item in pairs(ResouceManager.GetLoadablesForClient()) do 1092 | ResouceManager.LoadScriptOnClient(Item, Player) 1093 | end 1094 | end 1095 | end 1096 | 1097 | local function SetupPlayer(Player) 1098 | --- Setups up a player 1099 | -- @param Player The player to setup 1100 | 1101 | if Configuration.BlackList and CheckIfPlayerIsBlacklisted(Player, Configuration.BlackList) then 1102 | Player:Kick() 1103 | warn("Kicked Player " .. Player.Name .. " who was blacklisted") 1104 | 1105 | return nil 1106 | else 1107 | local PlayerSplashScreen 1108 | local HumanoidDiedEvent 1109 | local ExecutablesDumped = false 1110 | 1111 | local function SetupCharacter(ForceDumpExecutables) 1112 | --- Setup's up a player's character 1113 | -- @param ForceDumpExecutables Forces executables to be dumped, even if StarterGui.ResetPlayerGuiOnSpawn is true. 1114 | if not Configuration.CustomCharacters then 1115 | if Player and Player:IsDescendantOf(Players) then 1116 | while not (Player.Character 1117 | and Player.Character:FindFirstChild("Humanoid") 1118 | and Player.Character.Humanoid:IsA("Humanoid")) 1119 | and Player:IsDescendantOf(Players) do 1120 | 1121 | wait(0) -- Wait for the player's character to load 1122 | end 1123 | 1124 | -- Make sure the player is still in game. 1125 | if Player.Parent == Players then 1126 | if HumanoidDiedEvent then 1127 | HumanoidDiedEvent:disconnect() 1128 | HumanoidDiedEvent = nil 1129 | end 1130 | 1131 | HumanoidDiedEvent = Player.Character.Humanoid.Died:connect(function() 1132 | wait(Configuration.CharacterRespawnTime) 1133 | if Player:IsDescendantOf(Players) then 1134 | Player:LoadCharacter() 1135 | end 1136 | end) 1137 | 1138 | if not ExecutablesDumped or ForceDumpExecutables == true or StarterGui.ResetPlayerGuiOnSpawn then 1139 | wait() -- On respawn for some reason, this wait is needed. 1140 | DumpExecutables(Player) 1141 | ExecutablesDumped = true 1142 | end 1143 | else 1144 | print(Configuration.PrintHeader .. "is not int he game. Cannot finish load.") 1145 | end 1146 | else 1147 | print(Configuration.PrintHeader .. " is not in the game. Cannot load.") 1148 | end 1149 | end 1150 | end 1151 | 1152 | local function LoadSplashScreen() 1153 | --- Load's the splash screen into the player 1154 | 1155 | if Configuration.SplashScreen then 1156 | PlayerSplashScreen = GenerateInitialSplashScreen(Player) 1157 | 1158 | if Configuration.SoloTestMode then 1159 | Network.AddSplashToNevermore(NevermoreEngine) 1160 | end 1161 | end 1162 | end 1163 | 1164 | local function InitialCharacterLoad() 1165 | -- Makes sure the character loads, and sets up the character if it has already loaded 1166 | 1167 | if not Player.Character then -- Incase the characters do start auto-loading. 1168 | if PlayerSplashScreen then 1169 | PlayerSplashScreen.Parent = nil 1170 | end 1171 | 1172 | if Configuration.CustomCharacters then 1173 | Player:LoadCharacter() 1174 | if not Player.Character then 1175 | -- Player.CharacterAdded:wait() 1176 | 1177 | -- Fixes potential infinite yield. 1178 | while not Player.Character and Player:IsDescendantOf(Players) do 1179 | wait() 1180 | end 1181 | end 1182 | 1183 | if Player:IsDescendantOf(Players) then 1184 | Player.Character:Destroy() 1185 | end 1186 | else 1187 | Player:LoadCharacter() 1188 | end 1189 | 1190 | if PlayerSplashScreen then 1191 | PlayerSplashScreen.Parent = Player.PlayerGui 1192 | end 1193 | else 1194 | SetupCharacter(true) -- Force dump on the first load. 1195 | end 1196 | end 1197 | 1198 | -- WHAT MUST HAPPEN 1199 | -- Character must be loaded at least once 1200 | -- Nevermore must run on the client to get a splash running. However, this can be seen as optinoal. 1201 | -- Nevermore must load the splash into the player. 1202 | 1203 | -- SETUP EVENT FIRST 1204 | Player.CharacterAdded:connect(SetupCharacter) 1205 | InitialCharacterLoad() -- Force load the character, no matter what. 1206 | LoadSplashScreen() 1207 | end 1208 | end 1209 | 1210 | local function ConnectPlayers() 1211 | --- Connects all the events and adds players into the system. 1212 | 1213 | Network.ConnectPlayers = nil -- Only do this once! 1214 | 1215 | -- Setup all the players that joined... 1216 | for _, Player in pairs(game.Players:GetPlayers()) do 1217 | coroutine.resume(coroutine.create(function() 1218 | SetupPlayer(Player) 1219 | end)) 1220 | end 1221 | 1222 | -- And when they are added... 1223 | Players.PlayerAdded:connect(function(Player) 1224 | SetupPlayer(Player) 1225 | end) 1226 | end 1227 | Network.ConnectPlayers = ConnectPlayers 1228 | end 1229 | if Configuration.IsClient or Configuration.SoloTestMode then 1230 | -- Setup the Splash Screen. 1231 | -- However, in SoloTestMode, we need to setup the splashscreen in the 1232 | -- replicated storage module, which is technically the server module. 1233 | 1234 | local function AnimateSplashScreen(ScreenGui) 1235 | --- Creates a Windows 8 style loading screen, finishing the loading animation 1236 | -- of pregenerated spash screens. 1237 | -- @param ScreenGui The ScreenGui generated by the splash initiator. 1238 | 1239 | -- Will only be called if SpashScreens are enabled. 1240 | 1241 | local Configuration = { 1242 | OrbitTime = 5; -- How long the orbit should last. 1243 | OrbitTimeBetweenStages = 0.5; 1244 | Texture = "http://www.roblox.com/asset/?id=129689248"; -- AssetId of orbiting object (Decal) 1245 | ParticalOrbitDistance = 50; -- How far out the particals orbit 1246 | ParticalSize = 10; -- How big the particals are, probably should be an even number.. 1247 | ParticalCount = 5; -- How many particals to generate 1248 | ParticleSpacingTime = 0.25; -- How long to wait between earch partical before releasing the next one 1249 | } 1250 | 1251 | local Splash = {} 1252 | 1253 | local IsActive = true 1254 | local MainFrame = ScreenGui.SplashScreen 1255 | local ParticalFrame = MainFrame.ParticalFrame 1256 | local LogoLabel = MainFrame.LogoLabel 1257 | local ParticalList = {} 1258 | 1259 | local function Destroy() 1260 | -- Can be called to Destroy the SplashScreen. Will have the Alias 1261 | -- ClearSplash in NevermoreEngine 1262 | 1263 | IsActive = false; 1264 | MainFrame:Destroy() 1265 | 1266 | if ScreenGui then 1267 | ScreenGui:Destroy() 1268 | end 1269 | end 1270 | Splash.Destroy = Destroy 1271 | 1272 | local function SetParent(NewParent) 1273 | -- Used to fix rendering issues with multiple ScreenGuis. 1274 | MainFrame.Parent = NewParent 1275 | 1276 | if ScreenGui then 1277 | ScreenGui:Destroy() 1278 | ScreenGui = nil 1279 | end 1280 | end 1281 | Splash.SetParent = SetParent 1282 | 1283 | spawn(function() 1284 | LogoLabel.ImageTransparency = 1 1285 | ContentProvider:Preload(SplashConfiguration.LogoTexture) 1286 | while IsActive and (ContentProvider.RequestQueueSize > 0) do 1287 | RunService.RenderStepped:wait() 1288 | end 1289 | 1290 | spawn(function() 1291 | while IsActive and LogoLabel.ImageTransparency >= 0 do 1292 | LogoLabel.ImageTransparency = LogoLabel.ImageTransparency - 0.05 1293 | RunService.RenderStepped:wait() 1294 | end 1295 | LogoLabel.ImageTransparency = 0 1296 | end) 1297 | 1298 | if IsActive then 1299 | local function MakePartical(Parent, RotationRadius, Size, Texture) 1300 | -- Creates a partical that will circle around the center of it's Parent. 1301 | -- RotationRadius is how far away it orbits 1302 | -- Size is the size of the ball... 1303 | -- Texture is the asset id of the texture to use... 1304 | 1305 | -- Create a new ImageLabel to be our rotationg partical 1306 | local Partical = Instance.new("ImageLabel") 1307 | Partical.Name = "Partical"; 1308 | Partical.Size = UDim2.new(0, Size, 0, Size); 1309 | Partical.BackgroundTransparency = 1; 1310 | Partical.Image = Texture; 1311 | Partical.BorderSizePixel = 0; 1312 | Partical.ZIndex = 10; 1313 | Partical.Parent = Parent; 1314 | Partical.Visible = false; 1315 | 1316 | local ParticalData = { 1317 | Frame = Partical; 1318 | RotationRadius = RotationRadius; 1319 | StartTime = math.huge; 1320 | Size = Size; 1321 | SetPosition = function(ParticalData, CurrentPercent) 1322 | -- Will set the position of the partical relative to CurrentPercent. CurrentPercent @ 0 should be 0 radians. 1323 | 1324 | local PositionX = math.cos(math.pi * 2 * CurrentPercent) * ParticalData.RotationRadius 1325 | local PositionY = math.sin(math.pi * 2 * CurrentPercent) * ParticalData.RotationRadius 1326 | ParticalData.Frame.Position = UDim2.new(0.5 + PositionX/2, -ParticalData.Size/2, 0.5 + PositionY/2, -ParticalData.Size/2) 1327 | --ParticalData.Frame:TweenPosition(UDim2.new(0.5 + PositionX/2, -ParticalData.Size/2, 0.5 + PositionY/2, -ParticalData.Size/2), "Out", "Linear", 0.03, true) 1328 | end; 1329 | } 1330 | 1331 | return ParticalData; 1332 | end 1333 | 1334 | local function EaseOut(Percent, Amount) 1335 | -- Just return's the EaseOut smoothed out percentage 1336 | 1337 | return -(1 - Percent^Amount) + 1 1338 | end 1339 | 1340 | local function EaseIn(Percent, Amount) 1341 | -- Just return's the Easein smoothed out percentage 1342 | 1343 | return Percent^Amount 1344 | end 1345 | 1346 | local function EaseInOut(Percent, Amount) 1347 | -- Return's a smoothed out percentage, using in-out. 'Amount' 1348 | -- is the powered amount (So 2 would be a quadratic EaseInOut, 1349 | -- 3 a cubic, and so forth. Decimals supported) 1350 | 1351 | if Percent < 0.5 then 1352 | return ((Percent*2)^Amount)/2 1353 | else 1354 | return (-((-(Percent*2) + 2)^Amount))/2 + 1 1355 | end 1356 | end 1357 | 1358 | local function GetFramePercent(Start, Finish, CurrentPercent) 1359 | -- Return's the relative percentage to the overall 1360 | -- 'CurrentPercentage' which ranges from 0 to 100; So in one 1361 | -- case, 0 to 0.07, at 50% would be 0.035; 1362 | 1363 | return ((CurrentPercent - Start) / (Finish - Start)) 1364 | end 1365 | 1366 | local function GetTransitionedPercent(Origin, Target, CurrentPercent) 1367 | -- Return's the Transitional percentage (How far around the 1368 | -- circle the little ball is), when given a Origin ((In degrees) 1369 | -- and a Target (In degrees), and the percentage transitioned 1370 | -- between the two...) 1371 | 1372 | return (Origin + ((Target - Origin) * CurrentPercent)) / 360; 1373 | end 1374 | 1375 | -- Start the beautiful update loop 1376 | 1377 | -- Add / Create particals 1378 | for Index = 1, Configuration.ParticalCount do 1379 | ParticalList[Index] = MakePartical(ParticalFrame, 1, Configuration.ParticalSize, Configuration.Texture) 1380 | end 1381 | 1382 | local LastStartTime = 0; -- Last time a partical was started 1383 | local ActiveParticalCount = 0; 1384 | local NextRunTime = 0 -- When the particals can be launched again... 1385 | 1386 | while IsActive do 1387 | local CurrentTime = tick(); 1388 | for Index, Partical in ipairs(ParticalList) do 1389 | -- Calculate the CurrentPercentage from the time and 1390 | local CurrentPercent = ((CurrentTime - Partical.StartTime) / Configuration.OrbitTime); 1391 | 1392 | if CurrentPercent < 0 then 1393 | if LastStartTime + Configuration.ParticleSpacingTime <= CurrentTime and ActiveParticalCount == (Index - 1) and NextRunTime <= CurrentTime then 1394 | -- Launch Partical... 1395 | 1396 | Partical.Frame.Visible = true; 1397 | Partical.StartTime = CurrentTime; 1398 | LastStartTime = CurrentTime 1399 | ActiveParticalCount = ActiveParticalCount + 1; 1400 | 1401 | if Index == Configuration.ParticalCount then 1402 | NextRunTime = CurrentTime + Configuration.OrbitTime + Configuration.OrbitTimeBetweenStages; 1403 | end 1404 | Partical:SetPosition(45/360) 1405 | end 1406 | elseif CurrentPercent > 1 then 1407 | Partical.Frame.Visible = false; 1408 | Partical.StartTime = math.huge; 1409 | ActiveParticalCount = ActiveParticalCount - 1; 1410 | elseif CurrentPercent <= 0.08 then 1411 | Partical:SetPosition(GetTransitionedPercent(45, 145, EaseOut(GetFramePercent(0, 0.08, CurrentPercent), 1.2))) 1412 | elseif CurrentPercent <= 0.39 then 1413 | Partical:SetPosition(GetTransitionedPercent(145, 270, GetFramePercent(0.08, 0.39, CurrentPercent))) 1414 | elseif CurrentPercent <= 0.49 then 1415 | Partical:SetPosition(GetTransitionedPercent(270, 505, EaseInOut(GetFramePercent(0.39, 0.49, CurrentPercent), 1.1))) 1416 | elseif CurrentPercent <= 0.92 then 1417 | Partical:SetPosition(GetTransitionedPercent(505, 630, GetFramePercent(0.49, 0.92, CurrentPercent))) 1418 | elseif CurrentPercent <= 1 then 1419 | Partical:SetPosition(GetTransitionedPercent(630, 760, EaseOut(GetFramePercent(0.92, 1, CurrentPercent), 1.1))) 1420 | end 1421 | end 1422 | RunService.RenderStepped:wait() 1423 | end 1424 | end 1425 | end) 1426 | 1427 | return Splash 1428 | end 1429 | 1430 | local function SetupSplashScreenIfEnabled() 1431 | -- CLient stuff. 1432 | --- Sets up the Splashscreen if it's enabled, and returnst he disabling / removing function. 1433 | -- @return The removing function, even if the splashscreen doesn't exist. 1434 | 1435 | local LocalPlayer = Players.LocalPlayer 1436 | local PlayerGui = LocalPlayer:FindFirstChild("PlayerGui") 1437 | 1438 | while not LocalPlayer:FindFirstChild("PlayerGui") do 1439 | wait(0) 1440 | print("[NevermoreEngine] - Waiting for PlayerGui") 1441 | PlayerGui = LocalPlayer:FindFirstChild("PlayerGui") 1442 | end 1443 | 1444 | local SplashEnabled = false 1445 | 1446 | local ScreenGui = PlayerGui:FindFirstChild("SplashScreen") 1447 | local SplashScreen 1448 | 1449 | if Configuration.SplashScreen and ScreenGui then 1450 | SplashEnabled = true 1451 | SplashScreen = AnimateSplashScreen(ScreenGui) 1452 | elseif Configuration.SplashScreen and Configuration.EnableFiltering and not Configuration.SoloTestMode then 1453 | SplashEnabled = true 1454 | ScreenGui = GenerateInitialSplashScreen(LocalPlayer) 1455 | SplashScreen = AnimateSplashScreen(ScreenGui) 1456 | else 1457 | print(Configuration.PrintHeader .. "Splash Screen could not be found, or is not enabled") 1458 | end 1459 | 1460 | local function ClearSplash() 1461 | -- print(Configuration.PrintHeader .. "Clearing splash.") 1462 | 1463 | --- Destroys and stops all animation of the current splashscreen, if it exists. 1464 | if SplashScreen then 1465 | SplashEnabled = false 1466 | SplashScreen.Destroy() 1467 | SplashScreen = nil; 1468 | end 1469 | end 1470 | 1471 | local function GetSplashEnabled() 1472 | return SplashEnabled 1473 | end 1474 | 1475 | local function SetSplashParent(NewParent) 1476 | assert(NewParent:IsDescendantOf(game), Configuration.PrintHeader .. "New parent must be a descendant of of game") 1477 | 1478 | if SplashScreen then 1479 | SplashScreen.SetParent(NewParent) 1480 | else 1481 | warn(Configuration.PrintHeader .. "Could not set NewParent, Splash Screen could not be found, or is not enabled") 1482 | end 1483 | end 1484 | 1485 | return ClearSplash, GetSplashEnabled, SetSplashParent 1486 | end 1487 | -- Network.SetupSplashScreenIfEnabled = SetupSplashScreenIfEnabled 1488 | 1489 | local function AddSplashToNevermore(NevermoreEngine) 1490 | local ClearSplash, GetSplashEnabled, SetSplashParent = SetupSplashScreenIfEnabled() 1491 | 1492 | NevermoreEngine.SetSplashParent = SetSplashParent 1493 | NevermoreEngine.setSplashParent = SetSplashParent 1494 | 1495 | NevermoreEngine.ClearSplash = ClearSplash 1496 | NevermoreEngine.clearSplash = ClearSplash 1497 | NevermoreEngine.clear_splash = ClearSplash 1498 | 1499 | NevermoreEngine.GetSplashEnabled = GetSplashEnabled 1500 | NevermoreEngine.getSplashEnabled = GetSplashEnabled 1501 | NevermoreEngine.get_splash_enabled = GetSplashEnabled 1502 | end 1503 | Network.AddSplashToNevermore = AddSplashToNevermore 1504 | end 1505 | end 1506 | 1507 | --print(Configuration.PrintHeader .. "Loaded network module") 1508 | 1509 | ----------------------- 1510 | -- UTILITY NEVERMORE -- 1511 | ----------------------- 1512 | local SetRespawnTime 1513 | if Configuration.IsServer then 1514 | function SetRespawnTime(NewTime) 1515 | --- Sets how long it takes for a character to respawn. 1516 | -- @param NewTime The new time it takes for a character to respawn 1517 | if type(NewTime) == "number" then 1518 | Configuration.CharacterRespawnTime = NewTime 1519 | else 1520 | error(Configuration.PrintHeader .. " Could not set respawn time to '" .. tostring(NewTime) .. "', number expected, got '" .. type(NewTime) .. "'") 1521 | end 1522 | end 1523 | 1524 | --[[Network.GetMainDatastream().RegisterRequestTag(Configuration.NevermoreRequestPrefix .. "SetRespawnTime", function(Client, NewTime) 1525 | SetRespawnTime(NewTime) 1526 | end)--]] 1527 | --else 1528 | --[[function SetRespawnTime(NewTime) 1529 | --- Sends a request to the server to set the respawn time. 1530 | -- @param NewTime The new respawn time. 1531 | 1532 | Network.GetMainDatastream().Send(Configuration.NevermoreRequestPrefix .. "SetRespawnTime", NewTime) 1533 | end--]] 1534 | end 1535 | 1536 | --print(Configuration.PrintHeader .. "Loaded Nevermore Utilities") 1537 | 1538 | ------------------------ 1539 | -- INITIATE NEVERMORE -- 1540 | ------------------------ 1541 | 1542 | --print(Configuration.PrintHeader .. "Setup splashscreen") 1543 | 1544 | NevermoreEngine.SetRespawnTime = SetRespawnTime 1545 | NevermoreEngine.setRespawnTime = SetRespawnTime 1546 | NevermoreEngine.set_respawn_time = SetRespawnTime 1547 | 1548 | NevermoreEngine.GetResource = ResouceManager.GetResource 1549 | NevermoreEngine.getResource = ResouceManager.GetResource 1550 | NevermoreEngine.get_resource = ResouceManager.GetResource 1551 | 1552 | NevermoreEngine.LoadLibrary = ResouceManager.LoadLibrary 1553 | NevermoreEngine.loadLibrary = ResouceManager.LoadLibrary 1554 | NevermoreEngine.load_library = ResouceManager.LoadLibrary 1555 | 1556 | NevermoreEngine.ImportLibrary = ResouceManager.ImportLibrary 1557 | NevermoreEngine.importLibrary = ResouceManager.ImportLibrary 1558 | NevermoreEngine.import_library = ResouceManager.ImportLibrary 1559 | 1560 | NevermoreEngine.Import = ResouceManager.ImportLibrary 1561 | NevermoreEngine.import = ResouceManager.ImportLibrary 1562 | 1563 | -- These 2 following are used to get the raw objects. 1564 | NevermoreEngine.GetDataStreamObject = ResouceManager.GetDataStreamObject 1565 | NevermoreEngine.getDataStreamObject = ResouceManager.GetDataStreamObject 1566 | NevermoreEngine.get_data_stream_object = ResouceManager.GetDataStreamObject 1567 | 1568 | NevermoreEngine.GetRemoteFunction = ResouceManager.GetDataStreamObject -- Uh, yeah, why haven't I done this before? 1569 | 1570 | 1571 | NevermoreEngine.GetEventStreamObject = ResouceManager.GetEventStreamObject 1572 | NevermoreEngine.getEventStreamObject = ResouceManager.GetEventStreamObject 1573 | NevermoreEngine.get_event_stream_object = ResouceManager.GetEventStreamObject 1574 | 1575 | NevermoreEngine.GetRemoteEvent = ResouceManager.GetEventStreamObject 1576 | 1577 | 1578 | NevermoreEngine.GetDataStream = Network.GetDataStream 1579 | NevermoreEngine.getDataStream = Network.GetDataStream 1580 | NevermoreEngine.get_data_stream = Network.GetDataStream 1581 | 1582 | NevermoreEngine.GetEventStream = Network.GetEventStream 1583 | NevermoreEngine.getEventStream = Network.GetEventStream 1584 | NevermoreEngine.get_event_stream = Network.GetEventStream 1585 | 1586 | NevermoreEngine.SoloTestMode = Configuration.SoloTestMode -- Boolean value 1587 | 1588 | -- Internally used 1589 | NevermoreEngine.GetMainDatastream = Network.GetMainDatastream 1590 | 1591 | NevermoreEngine.NevermoreContainer = NevermoreContainer 1592 | NevermoreEngine.nevermoreContainer = NevermoreContainer 1593 | NevermoreEngine.nevermore_container = NevermoreContainer 1594 | 1595 | NevermoreEngine.ReplicatedPackage = ReplicatedPackage 1596 | NevermoreEngine.replicatedPackage = ReplicatedPackage 1597 | NevermoreEngine.replicated_package = ReplicatedPackage 1598 | 1599 | if Configuration.IsServer then 1600 | local function Initiate() 1601 | --- Called up by the loader. 1602 | 1603 | --- Initiates Nevermore. This should only be called once. 1604 | -- Since Nevermore sets all of its executables, and executes them manually, 1605 | -- there is no need to wait for Nevermore when these run. 1606 | 1607 | NevermoreEngine.Initiate = nil 1608 | 1609 | ResouceManager.PopulateResourceCache() 1610 | Network.ConnectPlayers() 1611 | ResouceManager.ExecuteExecutables() 1612 | end 1613 | NevermoreEngine.Initiate = Initiate 1614 | else 1615 | ResouceManager.PopulateResourceCache() 1616 | 1617 | -- Yield until player loads 1618 | assert(coroutine.resume(coroutine.create(function() 1619 | local LocalPlayer do 1620 | while not (LocalPlayer and LocalPlayer:IsA("Player")) do 1621 | warn(Configuration.PrintHeader, "Yielding until player is added. Currently found: " .. tostring(LocalPlayer)) 1622 | LocalPlayer = Players.LocalPlayer or Players.ChildAdded:wait() 1623 | end 1624 | end 1625 | 1626 | Network.AddSplashToNevermore(NevermoreEngine) 1627 | end))) 1628 | end 1629 | 1630 | --print(Configuration.PrintHeader .. "Nevermore is initiated successfully." 1631 | --print(Configuration.PrintHeader .. "#ReturnValues = ".. (#ReturnValues)) 1632 | 1633 | return NevermoreEngine 1634 | --------------------------------------------------------------------------------