Skip to content
Browse files

Remove build utilities; move stuff to root.

There are better solutions for building and bundling out there. I now
use browserify to get the job done. This makes Villain a whole lot
simpler, though it still has a ways to go.
  • Loading branch information...
1 parent 35181c2 commit 5d01ae3a28bc7a74aafef51b4b898483f893ca50 @stephank committed
View
2 .gitignore
@@ -1,2 +0,0 @@
-lib
-docs
View
22 Cakefile
@@ -1,22 +0,0 @@
-fs = require 'fs'
-coffee = require 'coffee-script'
-sys = require 'sys'
-
-task 'bootstrap', 'Build the Villain circular dependencies.', ->
- fs.mkdirSync(dir, 0777) for dir in ['lib', 'lib/build']
- for module in ['index', 'build/cake']
- modulePath = "src/#{module}.coffee"
- sys.puts "bootstrap : #{modulePath}"
- cscode = fs.readFileSync modulePath, 'utf-8'
- jscode = coffee.compile cscode, fileName: modulePath
- fs.writeFileSync "lib/#{module}.js", jscode, 'utf-8'
-
-task 'build', 'Compile the Villain modules.', ->
- try
- fs.statSync('lib/build/cake.js')
- catch e
- throw e unless e.errno == process.ENOENT
- invoke 'bootstrap'
-
- villain = require './lib/build/cake'
- villain.compileDirectory 'src', 'lib'
View
60 README.md
@@ -2,62 +2,6 @@
Villain is library / framework for real-time game development in JavaScript and [CoffeeScript].
Currently, villain consists of a set of base classes for real-time games, a set of subclasses that
-implement networking, and build tools to enable code-sharing between server and browser.
+implement networking.
-Villain was extracted from [Orona].
-
-## Usage
-
-You need [node.js] and [npm]. Then, simply:
-
- npm install villain coffee-script
- villain path/to/project/directory ProjectName
-
-Documentation is scarce, and the generator currently builds a very rough template. But then, you
-came here to see a 0.1 release.
-
-CoffeeScript is really only a recommendation, not a dependency. There's currently no pure
-JavaScript generator, however. If you plan on installing from Git, you *do* need CoffeeScript.
-
-## License
-
-Villain itself is MIT-licensed:
-
----
-
-© 2010 Stéphan Kochen
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
-associated documentation files (the "Software"), to deal in the Software without restriction,
-including without limitation the rights to use, copy, modify, merge, publish, distribute,
-sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or
-substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
-NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
-OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
----
-
-Some files, or parts of files, were taken from other projects:
-
-* `villain/util/events` was only slightly modified from [node.js]'s `events.js`
- (© 2010 Ryan Dahl, MIT-licensed).
-
-* Parts of the `villain/build/cake` are based on [CoffeeScript]'s `command.coffee`
- (© 2010 Jeremy Ashkenas, MIT-licensed) and [Yabble]'s `yabbler.js`
- (© 2010 James Brantly, MIT-licensed).
-
-* The source file `villain/util/brequire` is a modification of [Brequire] (© 2010 Jonah Fox).
-
- [CoffeeScript]: http://jashkenas.github.com/coffee-script/
- [Orona]: http://github.com/stephank/orona
- [node.js]: http://nodejs.org/
- [npm]: http://github.com/isaacs/npm
- [Yabble]: http://github.com/jbrantly/yabble
- [Brequire]: http://github.com/weepy/brequire
+Villain was extracted from [Orona]. It is MIT-licensed.
View
7 bin/villain
@@ -1,7 +0,0 @@
-#!/usr/bin/env node
-
-var path = require('path');
-var fs = require('fs');
-var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib');
-
-require(lib + '/build/generator');
View
6 src/index.coffee → index.coffee
@@ -1,12 +1,6 @@
-fs = require 'fs'
-path = require 'path'
-
-
## Villain
# Villain is a library of modules each serving a distinct purpose. As such, there is (currently)
# not much of a main module. Instead, browse around the modules and classes to find what you need.
exports.VERSION = '0.1.3'
-
-exports.getLibraryPath = -> path.dirname(fs.realpathSync(__filename))
View
0 src/loop.coffee → loop.coffee
File renamed without changes.
View
21 package.json
@@ -1,8 +1,7 @@
{
"name": "villain",
- "version": "0.1.3",
+ "version": "0.2.0",
"description": "The evil library for real-time games.",
- "directories": { "lib" : "./lib" },
"homepage": "http://stephank.github.com/villain/",
"repository": {
@@ -16,22 +15,8 @@
},
"engines: ": {
- "node": ">=0.2.0"
+ "node": "0.4"
},
- "main": "./lib/index",
- "modules": {
- "loop": "./lib/loop",
- "struct": "./lib/struct",
- "build/cake": "./lib/build/cake",
- "world/object": "./lib/world/object",
- "world/local": "./lib/world/local",
- "world/net/object": "./lib/world/net/object",
- "world/net/local": "./lib/world/net/local",
- "world/net/server": "./lib/world/net/server",
- "world/net/client": "./lib/world/net/client"
- },
- "bin": {
- "villain": "./bin/villain"
- }
+ "main": "./index"
}
View
176 src/build/cake.coffee
@@ -1,176 +0,0 @@
-# This module contains helpers for Cakefiles of villain projects. Villain itself uses it too.
-
-fs = require 'fs'
-path = require 'path'
-constants = if process.ENOENT? then process else require 'constants'
-{spawn} = require 'child_process'
-coffee = require 'coffee-script'
-villain = require '../index'
-sys = require('sys')
-# FIXME: watch functionality, as in 'coffee -w ...'
-
-
-# Recursively compile coffee files in `indir`, place them in `outdir`.
-compileDirectory = (indir, outdir) ->
- try
- fs.mkdirSync outdir, 0777
- catch e # Assume already exists.
-
- for filename in fs.readdirSync(indir)
- inpath = path.join indir, filename
- outpath = path.join outdir, filename
- if filename.match /\.coffee$/
- sys.puts " coffee : #{inpath}"
- outpath = outpath.replace /\.coffee$/, '.js'
- cscode = fs.readFileSync inpath, 'utf-8'
- jscode = coffee.compile cscode, fileName: inpath
- fs.writeFileSync outpath, jscode, 'utf-8'
- else if filename.match /\.js$/
- sys.puts " js : #{inpath}"
- jscode = fs.readFileSync inpath, 'utf-8'
- fs.writeFileSync outpath, jscode, 'utf-8'
- else
- stats = fs.statSync inpath
- compileDirectory(inpath, outpath) if stats.isDirectory()
-
-# Return an array of the dependencies of a given module. The array contains pairs of the module
-# names and file names. The optional `env` is a hash mapping external libraries to their base
-# include path. Only relative dependencies and dependencies from `env` are included in the results.
-determineDependencies = (module, filename, code, env) ->
- env ||= {}
- retval = []
-
- re = /(?:^|[^\w\$_.])require\s*\(\s*("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')\s*\)/g
- while match = re.exec(code)
- requirepath = eval(match[1])
- requireParts = eval(match[1]).split('/')
-
- if requirepath.charAt(0) != '.'
- first = requireParts.shift()
- continue unless env.hasOwnProperty(first)
- fileParts = env[first].split('/')
- moduleParts = [first]
- else
- fileParts = filename.split('/'); fileParts.pop()
- moduleParts = module.split('/'); moduleParts.pop()
-
- for part in requireParts
- switch part
- when '.' then continue
- when '..' then moduleParts.pop(); fileParts.pop()
- else moduleParts.push(part); fileParts.push(part)
-
- retval.push [moduleParts.join('/'), fileParts.join('/')]
-
- retval
-
-# Wrap some JavaScript into a module transport definition.
-wrapModule = (module, code) ->
- """
- require.module('#{module}', function(module, exports, require) {
- #{code}
- });
-
- """
-
-# Iterate on the given module and its dependencies. This is an internal helper for `bundleSources`.
-iterateDependencyTree = (module, filename, state, cb) ->
- try
- if fs.statSync(filename).isDirectory()
- filename = path.join filename, 'index.js'
- catch e
- throw e unless e.errno == constants.ENOENT
- unless filename.match(/\.js$/)
- filename = "#{filename}.js"
- if filename.match(/\/index\.js$/) and not module.match(/\/index$/)
- module = path.join module, 'index'
- return if state.seen.indexOf(module) != -1
- state.seen.push module
-
- code = fs.readFileSync filename, 'utf-8'
- cb(module, filename, code)
-
- for [mod, file] in determineDependencies(module, filename, code, state.env)
- iterateDependencyTree(mod, file, state, cb)
- return
-
-# Create a bundle of sources, and write it to the stream `output`. The options contains:
-#
-# * `modules`: The base modules to compile. This is a mapping of module names to their source
-# files. All of these files will be inspected for dependencies and bundled.
-# * `additional`: Additional files to include verbatim. Unlike `modules`, these files are not
-# wrapped in a transport definition, and are included at the top of the bundle.
-# * `env`: A mapping of names of external libraries to their include paths. Normally, absolute
-# requires are skipped. Requires for libraries in `env` are the exception, and will be included
-# in the bundle, along with their dependencies.
-bundleSources = (output, options) ->
- modules = options.modules || {}
- env = options.env || {}
- additional = options.additional || []
-
- for filename in additional
- sys.puts " bundle : #{filename}"
- code = fs.readFileSync filename, 'utf-8'
- output.write code
-
- state = {}
- state.env = options.env || {}
- state.seen = []
-
- for module, filename of modules
- iterateDependencyTree module, filename, state, (mod, file, code) ->
- sys.puts " bundle : #{file}"
- wrapped = wrapModule mod, code
- output.write wrapped
-
- return
-
-# Wraps a writable stream with a JavaScript compressor. The compressor is activated by the user
-# using environment variables `CLOSURE` or `UGLIFYJS`.
-createCompressorStream = (wrappee) ->
- sub = null
- if closure = process.env.CLOSURE
- sub = spawn 'java', ['-jar', closure]
- name = 'UglifyJS'
- else if uglifyjs = process.env.UGLIFYJS
- sub = spawn 'node', [uglifyjs]
- name = 'Closure'
-
- if sub
- realEnd = sub.end
- sub.end = ->
- sys.puts " compress : #{name}"
- realEnd.apply sub, arguments
-
- sub.stdout.on 'data', (buffer) -> wrappee.write buffer
- sub.stderr.on 'data', (buffer) -> process.stdout.write buffer
- sub.on 'exit', -> wrappee.end()
- sub.stdin
- else
- wrappee
-
-# The helper used to build the client bundle in a basic Villain project set-up. It includes the
-# browser-side CommonJS and EventEmitter libraries, bundles required Villain sources, and applies
-# a compressor if one was specified.
-simpleBundle = (bundlepath, modules) ->
- output = createCompressorStream fs.createWriteStream bundlepath
- villainLib = villain.getLibraryPath()
- bundleSources output,
- env:
- 'villain': villainLib
- 'events': path.join(villainLib, 'util', 'events.js')
- modules: modules
- additional: [
- path.join(villainLib, 'util', 'brequire.js')
- ]
- output.end()
-
-
-## Exports
-
-exports.compileDirectory = compileDirectory
-exports.determineDependencies = determineDependencies
-exports.wrapModule = wrapModule
-exports.bundleSources = bundleSources
-exports.createCompressorStream = createCompressorStream
-exports.simpleBundle = simpleBundle
View
277 src/build/generator.coffee
@@ -1,277 +0,0 @@
-# The implementation of the `villain` command-line application, which generates a basic project
-# from a template.
-
-sys = require 'sys'
-fs = require 'fs'
-constants = if process.ENOENT? then process else require 'constants'
-{VERSION} = require '../index'
-
-
-## Arguments
-
-abort = (msg) ->
- sys.error(msg)
- process.exit(1)
-
-usage =
- """
- Usage: villain [options] PATH [PROJECT]
-
- PROJECT is a name for your project that will be used throughout the
- source code. You should specify a name in CamelCase.
-
- Options:
- -v, --version Output framework version
- -h, --help Output help information
- """
-
-args = process.argv.slice(2)
-path = null; project = null
-for arg in process.argv.slice(2)
- if arg == '-h' or arg == '--help'
- abort(usage)
- if arg == '-v' or arg == '--version'
- abort(VERSION)
-
- if project
- abort(usage)
- else if path
- project = arg
- else
- path = arg
-
-abort(usage) unless path
-
-unless project
- project = path.split('/').pop()
- project = project.split('_')
- project = for part in project
- part.charAt(0).toUpperCase() + part.slice(1)
- project = project.join('')
-unless project.match /^[a-z_\$][a-z0-9_\$]+$/i
- abort(
- """
- '#{project}' is not a valid project name.
- The project name has to be a valid JavaScript variable name.
- """)
-projectlc = project.toLowerCase()
-
-
-## Templates
-
-tmplCakefile =
- """
- # A basic Cakefile which compiles CoffeeScript sources for the server,
- # and packages them for the client in a single JavaScript bundle.
-
- villain = require 'villain/build/cake'
-
- # A task that recreates the `src/` directory structure under `lib/`, and
- # compiles any CoffeeScript in the process.
- task 'build:modules', 'Compile all #{project} modules', ->
- villain.compileDirectory 'src', 'lib'
-
- # A task that takes the modules from `build:modules`, and packages them
- # as a JavaScript bundle for shipping to the browser client.
- task 'build:client', 'Compile the #{project} client bundle', ->
- invoke 'build:modules'
-
- villain.simpleBundle 'public/#{projectlc}-bundle.js',
- '#{projectlc}/client': './lib/client/index.js'
-
- # The conventional default target.
- task 'build', 'Compile #{project}', ->
- invoke 'build:client'
- """
-
-tmplServerLauncher =
- """
- #!/usr/bin/env node
-
- // This is the server executable.
-
- // First, find the path to our include directory.
- var path = require('path');
- var fs = require('fs');
- var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib');
-
- // Then, instantiate and start the server.
- var #{project}ServerWorld = require(lib + '/server');
- var server = new #{project}ServerWorld();
- server.start();
- """
-
-tmplClientIndex =
- """
- ClientWorld = require 'villain/world/net/client'
- objects = require '../objects/all'
-
-
- class #{project}ClientWorld extends ClientWorld
-
- objects.registerWithWorld #{project}ClientWorld.prototype
-
-
- class Client
-
- constructor: ->
- @world = new #{project}ClientWorld()
-
- start: ->
-
-
- module.exports = Client
- """
-
-tmplServerIndex =
- """
- ServerWorld = require 'villain/world/net/server'
- objects = require '../objects/all'
-
-
- class #{project}ServerWorld extends ServerWorld
-
- objects.registerWithWorld #{project}ServerWorld.prototype
-
-
- class Server
-
- constructor: ->
- @world = new #{project}ServerWorld()
-
- start: ->
-
-
- module.exports = Server
- """
-
-tmplBaseObject =
- """
- NetWorldObject = require 'villain/world/net/object'
-
-
- # Your base class for all game objects.
- class #{project}Object extends NetWorldObject
-
-
- module.exports = #{project}Object
- """
-
-tmplExampleObject =
- """
- #{project}Object = require '../object'
-
-
- # An example object, that doesn't really do anything.
- class Example extends #{project}Object
-
- spawn: (arg1, arg2) ->
-
- update: ->
-
-
- module.exports = Example
- """
-
-tmplObjectsIndex =
- """
- # This is an index of all objects types in the game. It is important for
- # networking that objects are registered in the same order with the
- # `ServerWorld` on the server and the `ClientWorld` on the client.
- # Therefor, you want to use a function like this everywhere.
- exports.registerWithWorld = (w) ->
- w.registerType require './example'
- """
-
-tmplIndexPage =
- """
- <!DOCTYPE html>
- <html>
- <head>
- <title>#{project}</title>
- <script src="#{projectlc}-bundle.js"></script>
- <script>
- (function(){
- var #{project}ClientWorld = require("#{projectlc}/client");
- var world = new #{project}ClientWorld();
- world.start();
- })();
- </script>
- </head>
- <body>
- </body>
- </html>
- """
-
-
-## Build directory structure
-
-# Whether the target directory exists and is empty.
-emptyDirectory = ->
- empty = yes
- try
- empty = (fs.readdirSync(path).length == 0)
- catch e
- throw e unless e.errno == constants.ENOENT
- empty
-
-# Create the stub application from the templates.
-createApplication = ->
- mkdir "#{path}"
- write "#{path}/Cakefile", tmplCakefile
- mkdir "#{path}/bin"
- write "#{path}/bin/#{projectlc}-server", tmplServerLauncher
- mkdir "#{path}/src"
- write "#{path}/src/object.coffee", tmplBaseObject
- mkdir "#{path}/src/client"
- write "#{path}/src/client/index.coffee", tmplClientIndex
- mkdir "#{path}/src/server"
- write "#{path}/src/server/index.coffee", tmplServerIndex
- mkdir "#{path}/src/objects"
- write "#{path}/src/objects/all.coffee", tmplObjectsIndex
- write "#{path}/src/objects/example.coffee", tmplExampleObject
- mkdir "#{path}/public"
- write "#{path}/public/index.html", tmplIndexPage
-
-# Create a directory, if it does not exist.
-mkdir = (path) ->
- try
- fs.mkdirSync path, 0777
- catch e
- throw e unless e.errno == constants.EEXIST
- sys.puts " dir : #{path}"
-
-# Write a file, if it does not exist.
-write = (path, str) ->
- try
- fs.statSync path
- catch e
- throw e unless e.errno == constants.ENOENT
- fs.writeFileSync path, str, 'utf-8'
- sys.puts " create : #{path}"
- return
- sys.puts " exists : #{path}"
-
-# Ask for confirmation to a question.
-confirm = (msg, fn) ->
- prompt msg, (val) ->
- fn(/^ *y(es)?/i.test(val))
-
-# Prompt the user for input.
-stdin = null
-prompt = (msg, fn) ->
- stdin ||= process.openStdin()
- sys.print msg
- stdin.setEncoding 'ascii'
- stdin.addListener 'data', (data) ->
- fn(data)
- stdin.removeListener 'data', arguments.callee
-
-# Entry-point
-if emptyDirectory(path)
- createApplication()
-else
- confirm 'destination is not empty, continue? ', (isOkay) ->
- abort 'aborted' unless isOkay
- createApplication()
- stdin.destroy()
View
45 src/util/brequire.coffee
@@ -1,45 +0,0 @@
-# Brequire - CommonJS support for the browser.
-# This version is slightly modified, and rewritten in CoffeeScript.
-
-
-# The require function loads the module on-demand, or returns the existing `exports` object in case
-# the module is already loaded.
-require = (path) ->
- originalPath = path
- unless m = require.modules[path]
- path += '/index'
- unless m = require.modules[path]
- throw "Couldn't find module for: #{originalPath}"
-
- unless m.exports
- m.exports = {}
- m.call m.exports, m, m.exports, require.bind(path)
-
- m.exports
-
-# Our index of modules.
-require.modules = {}
-
-# Helper used to create the `require` function used in the inner scope of the module. It takes
-# care of making paths relative to the current module work as expected.
-require.bind = (path) ->
- (p) ->
- return require(p) unless p.charAt(0) == '.'
-
- cwd = path.split('/')
- cwd.pop()
-
- for part in p.split('/')
- if part == '..' then cwd.pop()
- else unless part == '.' then cwd.push(part)
-
- require cwd.join('/')
-
-# The function used to define a module. Each module that is loaded into the browser should be
-# wrapped with a call to this function.
-require.module = (path, fn) ->
- require.modules[path] = fn
-
-
-#### Exports
-window.require = require
View
121 src/util/events.js
@@ -1,121 +0,0 @@
-// This is an extract from node.js, which is MIT-licensed.
-// © 2009, 2010 Ryan Lienhart Dahl.
-// Slightly adapted for a browser environment by Stéphan Kochen.
-
-var EventEmitter = exports.EventEmitter = function() {};
-
-var isArray = Array.isArray || function(o) {
- return Object.prototype.toString.call(o) === '[object Array]';
-};
-
-EventEmitter.prototype.emit = function (type) {
- // If there is no 'error' event listener then throw.
- if (type === 'error') {
- if (!this._events || !this._events.error ||
- (isArray(this._events.error) && !this._events.error.length))
- {
- if (arguments[1] instanceof Error) {
- throw arguments[1];
- } else {
- throw new Error("Uncaught, unspecified 'error' event.");
- }
- return false;
- }
- }
-
- if (!this._events) return false;
- var handler = this._events[type];
- if (!handler) return false;
-
- if (typeof handler == 'function') {
- if (arguments.length <= 3) {
- // fast case
- handler.call(this, arguments[1], arguments[2]);
- } else {
- // slower
- var args = Array.prototype.slice.call(arguments, 1);
- handler.apply(this, args);
- }
- return true;
-
- } else if (isArray(handler)) {
- var args = Array.prototype.slice.call(arguments, 1);
-
-
- var listeners = handler.slice();
- for (var i = 0, l = listeners.length; i < l; i++) {
- listeners[i].apply(this, args);
- }
- return true;
-
- } else {
- return false;
- }
-};
-
-// EventEmitter is defined in src/node_events.cc
-// EventEmitter.prototype.emit() is also defined there.
-EventEmitter.prototype.addListener = function (type, listener) {
- if ('function' !== typeof listener) {
- throw new Error('addListener only takes instances of Function');
- }
-
- if (!this._events) this._events = {};
-
- // To avoid recursion in the case that type == "newListeners"! Before
- // adding it to the listeners, first emit "newListeners".
- this.emit("newListener", type, listener);
-
- if (!this._events[type]) {
- // Optimize the case of one listener. Don't need the extra array object.
- this._events[type] = listener;
- } else if (isArray(this._events[type])) {
- // If we've already got an array, just append.
- this._events[type].push(listener);
- } else {
- // Adding the second element, need to change to array.
- this._events[type] = [this._events[type], listener];
- }
-
- return this;
-};
-
-EventEmitter.prototype.on = EventEmitter.prototype.addListener;
-
-EventEmitter.prototype.removeListener = function (type, listener) {
- if ('function' !== typeof listener) {
- throw new Error('removeListener only takes instances of Function');
- }
-
- // does not use listeners(), so no side effect of creating _events[type]
- if (!this._events || !this._events[type]) return this;
-
- var list = this._events[type];
-
- if (isArray(list)) {
- var i = list.indexOf(listener);
- if (i < 0) return this;
- list.splice(i, 1);
- if (list.length == 0)
- delete this._events[type];
- } else if (this._events[type] === listener) {
- delete this._events[type];
- }
-
- return this;
-};
-
-EventEmitter.prototype.removeAllListeners = function (type) {
- // does not use listeners(), so no side effect of creating _events[type]
- if (type && this._events && this._events[type]) this._events[type] = null;
- return this;
-};
-
-EventEmitter.prototype.listeners = function (type) {
- if (!this._events) this._events = {};
- if (!this._events[type]) this._events[type] = [];
- if (!isArray(this._events[type])) {
- this._events[type] = [this._events[type]];
- }
- return this._events[type];
-};
View
0 src/struct.coffee → struct.coffee
File renamed without changes.
View
0 src/world/base.coffee → world/base.coffee
File renamed without changes.
View
0 src/world/index.coffee → world/index.coffee
File renamed without changes.
View
0 src/world/net/client.coffee → world/net/client.coffee
File renamed without changes.
View
0 src/world/net/local.coffee → world/net/local.coffee
File renamed without changes.
View
0 src/world/net/object.coffee → world/net/object.coffee
File renamed without changes.
View
0 src/world/net/server.coffee → world/net/server.coffee
File renamed without changes.
View
0 src/world/object.coffee → world/object.coffee
File renamed without changes.

0 comments on commit 5d01ae3

Please sign in to comment.
Something went wrong with that request. Please try again.