diff --git a/.github/workflows/build-action.yml b/.github/workflows/build-action.yml new file mode 100644 index 0000000..24651f4 --- /dev/null +++ b/.github/workflows/build-action.yml @@ -0,0 +1,52 @@ +name: build-action +on: [push] +defaults: + run: + working-directory: ./modules/web-ui +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + + - name: Install npm dependencies + run: npm install + + - name: Build & run + run: docker build . + + # - name: SonarCloud Scan + # uses: sonarsource/sonarcloud-github-action@master + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + - name: Start Mosquitto + uses: namoshek/mosquitto-github-action@v1 + with: + version: '1.6' + ports: '1883:1883 9001:9001' + certificates: ${{ github.workspace }}/modules/web-ui/.ci/tls-certificates + config: ${{ github.workspace }}/modules/web-ui/.ci/mosquitto.conf + container-name: 'mqtt' + + - name: Wait a bit until MQTT broker has started + run: sleep 30 + + # - name: Cypress Test + # run: npm run cytest + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + - name: Push container to GHCR + uses: docker/build-push-action@v2 + with: + context: ./modules/web-ui + push: true + tags: ghcr.io/cmcrobotics/microsquad-web-ui:latest diff --git a/.gitignore b/.gitignore index 899dafa..6476bc2 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,10 @@ venv/ ENV/ env.bak/ venv.bak/ +virtual-env/ + +# Bitio cache file +portscan.cache # Spyder project settings .spyderproject @@ -131,3 +135,7 @@ dmypy.json .vscode/ .vscode/cpx.json modules/gateway/src/main/python/microsquad/portscan.cache + +# ignore virtual environments +**/*-venv/* +node/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e2c2afc --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,62 @@ + + +.openshift_auth_setup: &openshift_auth_setup + image: gitlab-registry.cern.ch/paas-tools/openshift-client:latest + before_script: + - echo "Sourcing SCM branch environment at modules/web-ui/deployment/cern-oc/${CI_COMMIT_REF_NAME}.env" + - source modules/web-ui/deployment/cern-oc/deployment/${CI_COMMIT_REF_NAME}.env + - export ENV_FILE_VAR="${CI_COMMIT_REF_NAME^^}_ENV" + - echo "Sourcing ${ENV_FILE_VAR} into .env file " + - cat ${!ENV_FILE_VAR} > .env + - source .env + - echo "Authenticating with ${OPENSHIFT_SERVER}" + - oc login $OPENSHIFT_SERVER --token=$OPENSHIFT_TOKEN + - oc project $NAMESPACE + + +sonarqube-check: + image: + name: sonarsource/sonar-scanner-cli:latest + entrypoint: [""] + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + cache: + key: "${CI_JOB_NAME}" + paths: + - .sonar/cache + script: + - sonar-scanner + allow_failure: true + only: + - develop + - master + + + +Build Web UI image: + stage: build + image: + name: gitlab-registry.cern.ch/ci-tools/docker-image-builder + entrypoint: [""] + script: + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json + - echo Building latest Docker Image $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME + - /kaniko/executor --context $CI_PROJECT_DIR/modules/web-ui --dockerfile $CI_PROJECT_DIR/modules/web-ui/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME + only: + - master + - develop + + +Update deployment: + <<: *openshift_auth_setup + stage: deploy + script: + - echo "***** Updating instance deployment for ${CI_COMMIT_REF_NAME}" + - oc process --ignore-unknown-parameters -o json --param-file=deployment/${CI_COMMIT_REF_NAME}.env -f modules/web-ui/deployment/cern-oc/service.yml --local=true | jq '.items[] | select (.kind != "PersistentVolumeClaim")' | oc apply -f - + - oc process --ignore-unknown-parameters --param-file=deployment/${CI_COMMIT_REF_NAME}.env -f modules/web-ui/deployment/cern-oc/routes.yml --local=true | oc apply -f - + only: + refs: [develop, master] + changes: + - "modules/web-ui/deployment/cern-oc/**/*" + diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..b901097 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..2cc7d4a Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..642d572 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/README.md b/README.md index 0daed73..5c04142 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # MicroSquad (a.k.a uSquad) +[![Known Vulnerabilities](https://snyk.io/test/github/lucasvanmol/usquad-web-ui/badge.svg)](https://snyk.io/test/github/lucasvanmol/usquad-web-ui) +![Build](https://github.com/cmcrobotics/microsquad/workflows/build-action/badge.svg) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=lucasvanmol_usquad-web-ui&metric=alert_status)](https://sonarcloud.io/dashboard?id=lucasvanmol_usquad-web-ui) + A Microbit orchestration library based on [Bitio](https://github.com/AdventuresInMinecraft/bitio) : Using a single Microbit as a gateway, control remote Microbits via the radio. Messages are exchanged using the Influx line protocol (with a small custom parser implemented in micropython) @@ -15,14 +19,21 @@ Messages are exchanged using the Influx line protocol (with a small custom parse # Dependencies For the **uSquad** Gateway : -* Python3 +* Python 3.8+ * [https://github.com/AdventuresInMinecraft/Bitio](https://github.com/AdventuresInMinecraft/bitio) -* A Microbit running the bitio firmware -* [Line Protocol parser for Python](https://pypi.org/project/influx-line-protocol/) For the **uSquad** clients : * The provided **uSquad** firmware to upload on each Microbit. +For the **uSquad** Web Interface : +* The spectacular [Kenney Character Assets](https://kenney.itch.io/kenney-character-assets) under Creative Commons Zero + # How to use it -# How to develop \ No newline at end of file +## Flash the Microbits + +## Start the Gateway + +## Connect to the administration web interface + +# How to develop diff --git a/docs/plantuml/game-management.puml b/docs/plantuml/game-management.puml new file mode 100644 index 0000000..2e8f192 --- /dev/null +++ b/docs/plantuml/game-management.puml @@ -0,0 +1,60 @@ +@startuml + + +actor "Game Organiser" as Organiser +actor Player + +participant "Game Manager" as Manager +control Gateway +database "Game Session" as Game +queue Broker +participant "Web Display" as Web + +activate Organiser + + +Organiser -> Gateway : Start session +activate Gateway + +activate Broker + +Gateway -> Game : Declare Game +activate Game +Gateway -> Broker : Declare namespace + +Gateway --> Organiser : Admin Code + +Organiser -> Manager : Run Game +activate Manager +Manager --> Organiser : Audience Code +Manager --> Gateway : Audience Code + +Manager -> Gateway : Send Game Logic + +Organiser -> Web : Join Game Admin Display +activate Web +Player --> Web : Join Game Audience Display + +Web -> Broker : Subscribe +Organiser -> Player : Start +activate Player +Player -> Gateway : Initiate connection +Gateway -> Broker : Declare device + +loop Game phase + Organiser -> Gateway : Start Phase + loop Player interactions + Player -> Gateway : Interact + Gateway -> Game : Update State + Gateway -> Broker : Broadcast update + Broker -> Web : Update Display + end + Organiser -> Gateway : End Phase +end +deactivate Player +Organiser -> Gateway : End Game +Gateway -> Game : Close Session +Gateway -> Broker : Release namespace +deactivate Game +Broker -> Web : Display Game Stats +@enduml \ No newline at end of file diff --git a/docs/plantuml/game-management/game-management.png b/docs/plantuml/game-management/game-management.png new file mode 100644 index 0000000..0b1df24 Binary files /dev/null and b/docs/plantuml/game-management/game-management.png differ diff --git a/docs/plantuml/gateway-components.puml b/docs/plantuml/gateway-components.puml new file mode 100644 index 0000000..aed14a4 --- /dev/null +++ b/docs/plantuml/gateway-components.puml @@ -0,0 +1,23 @@ +@startuml Gateway components + +title Gateway components +frame "Connector Scheduler"{ + [Connector] <> + [Gateway] <> + [Terminal] <> + USB -left-> Gateway + USB --> Connector + Radio <-> Gateway + Radio <--> Terminal +} +[MQTT Client] <> as mqtt + +frame "Mapper scheduler"{ + [Mapper] + RxPy -right-> Mapper + RxPy -up-> Connector +Mapper -> Homie +} +Homie --> mqtt + +@enduml \ No newline at end of file diff --git a/docs/plantuml/gateway-components/Gateway components.png b/docs/plantuml/gateway-components/Gateway components.png new file mode 100644 index 0000000..17b20e9 Binary files /dev/null and b/docs/plantuml/gateway-components/Gateway components.png differ diff --git a/modules/client-micropython/src/main/micropython/heart.py b/modules/client-micropython/src/main/micropython/heart.py new file mode 100644 index 0000000..3a18c2a --- /dev/null +++ b/modules/client-micropython/src/main/micropython/heart.py @@ -0,0 +1,4 @@ +from microbit import display, Image + + +display.show(Image.HEART) \ No newline at end of file diff --git a/modules/client/src/main/micropython/leftover.py b/modules/client-micropython/src/main/micropython/leftover.py similarity index 51% rename from modules/client/src/main/micropython/leftover.py rename to modules/client-micropython/src/main/micropython/leftover.py index 50bd759..3fbb56e 100644 --- a/modules/client/src/main/micropython/leftover.py +++ b/modules/client-micropython/src/main/micropython/leftover.py @@ -20,9 +20,43 @@ # elif SIMULATOR == True: # Debug # usquad_text({"value":measurement,"wait":"true"} ) +# def usquad_text(tags, timestamp=None): +# text_str = tags['value'].replace("_", " ") +# _delay = int(tags.get('delay',50)) +# _wait = (tags.get('wait', "true").lower()=="true") +# _clear = (tags.get('clear', "true").lower()=="true") +# display.show(text_str, delay=_delay, wait=_wait, clear=_clear) + +# def usquad_read_accel(tags= None, timestamp=None): +# x,y,z = accelerometer.get_values() +# usquad_send("read_accel", tags = {"x":x,"y":y,"z":z}) + +# def usquad_device_id(tags, timestamp=None): +# global DEVID +# DEVID = tags.get('id',machine.unique_id()) + +# def usquad_image(tags, timestamp=None): +# images_str = tags['value'] +# img = [(Image(img_str)) for img_str in images_str.split(";")] +# _del = int(tags.get('delay',1000)) +# _slp = int(tags.get('sleep',2000)) +# _wait = (tags.get('wait', "true").lower()=="true") +# _clr = (tags.get('clear', "false").lower()=="true") +# display.show(img, delay=_del, wait=_wait, clear=_clr) +# sleep(_slp) #incoming = 'image,value="99999:99999:99099:99999:99999;99999:55555:55055:55555:99999",delay=500,clear=false,wait=true' #incoming = 'text,value="Show_me_the_money",clear=true,wait=true' #incoming = 'accel' #incoming = 'vote,value="99999:99999:99099:99999:99999;99999:55555:55055:55555:99999;55555:50005:00000:50005:55555",duration=4000,votes=4' + + +# METHOD_MAP = { +# 'image' : usquad_image, +# 'accel' : usquad_read_accel, +# 'text' : usquad_text, +# 'vote' : usquad_vote, +# 'device_id' : usquad_device_id, +# 'buttons' : usquad_buttons +# } \ No newline at end of file diff --git a/modules/client-micropython/src/main/micropython/main.py b/modules/client-micropython/src/main/micropython/main.py new file mode 100644 index 0000000..8ac2076 --- /dev/null +++ b/modules/client-micropython/src/main/micropython/main.py @@ -0,0 +1,165 @@ +from microbit import display,Image,sleep, button_a, button_b, running_time +from micropython import const +import radio + + +import machine + +DEVID = const("{:x}".format(int.from_bytes(machine.unique_id(), "big"))) + +radio.config(channel=12, group=12, length=64, queue=2, power=4) +radio.on() + +_ELECTRON = (Image.ANGRY,[Image.ARROW_SE,Image.DIAMOND_SMALL,Image.HEART]) +_PROTON = (Image.SILLY,[Image.ARROW_SW,Image.TARGET,Image.HEART]) +_PHOTON = (Image.RABBIT,[Image.ARROW_S,Image.DIAMOND_SMALL,Image.HEART]) +_NEUTRON = (Image.PACMAN,[Image.ARROW_S,Image.TARGET,Image.HEART]) +_POSITRON = (Image.ASLEEP,[Image.ARROW_SW,Image.DIAMOND_SMALL, Image.HEART_SMALL]) +_ANTIPROTON = (Image.SURPRISED,[Image.ARROW_SE, Image.TARGET, Image.HEART_SMALL]) + +_PARTICLES = [_ELECTRON,_PROTON,_PHOTON,_NEUTRON, _POSITRON, _ANTIPROTON] + + +def _pop_or_none(arr): + if arr and len(arr)>0: + return arr.pop(0) + else: + return None + +def ulp_parse(msg): + meas = None + tags = {} + tmstp = None + + frags = msg.split(" ") + frag = _pop_or_none(frags) + if frag is not None: + measFrags = frag.split(",") + if len(measFrags) > 0: + meas = measFrags[0] + if len(measFrags) > 1: + for tagFrag in measFrags[1:]: + tagKV = tagFrag.split("=") + tags[tagKV[0]] = tagKV[1].strip('"\'') + frag = _pop_or_none(frags) + if frag is not None: + tmstp = int(frag) + return (meas, tags, tmstp) + +def ulp_serialize(measurement, tags=None, timestamp=None): + result = measurement + if tags is not None: + for key, value in tags.items(): + result += ','+str(key)+"=\""+str(value)+"\"" + return result + +def usquad_send(measurement, tags= {}, timestamp=None): + tags["dev_id"] = DEVID + msg = ulp_serialize(measurement, tags, timestamp) + radio.send(msg) + +def usquad_show(tags, timestamp=None): + particle_idx = int(tags['p']) + visual_idx = int(tags['v']) + display.show(_PARTICLES[particle_idx][1][visual_idx], delay=1000, wait=True, clear=False) + sleep(1000) + +def usquad_vote(tags,timestamp=None): + _vote(tags, [particle[0] for particle in _PARTICLES], timestamp) + +def usquad_emote(tags,timestamp=None): + _vote(tags, [Image.HEART,Image.SAD,Image.HAPPY,Image.SKULL], timestamp) + +def usquad_alive(tags,timestamp=None): + display.show(Image.CHESSBOARD) + +def _vote(tags,choices:list,timestamp=None): + global incoming,METHOD_LIST + _max_votes = int(tags.get('v',1)) + vote_cn = 0 + choices_max = len(choices) + choice = 0 + button_a.get_presses() + button_b.was_pressed() + stopVote = False + display.show(choices[choice], delay=50, clear=False,wait=True) + while (not stopVote) and (vote_cn < _max_votes): + a_presses = button_a.get_presses() + if a_presses > 0: + choice = (choice + a_presses) % choices_max + display.show(choices[choice]) + if button_b.was_pressed(): + usquad_send("read_vote",{"value":choice, "index":vote_cn}) + display.show(Image.ARROW_N, wait=True, clear=False) + sleep(300) + vote_cn += 1 + votes_left = _max_votes - vote_cn + if(votes_left > 0): + display.show(str(votes_left), clear=False, wait=True) + sleep(500) + display.show(choices[choice], clear=False,wait=False) + poll_messages() + if incoming is not None and (not(incoming.startswith("read_") or incoming.startswith("bonjour"))) and (ulp_parse(incoming)[0] in METHOD_LIST): + stopVote = True # keep incoming + display.show(Image.YES) + +def usquad_buttons(tags = None, timestamp=None): + global incoming,METHOD_LIST + button_a.was_pressed() + button_b.was_pressed() + display.show(Image.TRIANGLE) + stopBtn = False + while not stopBtn: + if button_a.was_pressed(): + usquad_send("read_button",{"button":"a"}) + display.show("a") + if button_b.was_pressed(): + usquad_send("read_button",{"button":"b"}) + display.show("b") + poll_messages() + if incoming is not None and (not(incoming.startswith("read_") or incoming == "bonjour")) and (ulp_parse(incoming)[0] in METHOD_LIST): + stopBtn = True # keep incoming + else: + sleep(250) + display.show(Image.SQUARE_SMALL) + + +METHOD_MAP = const({ + 'show' : usquad_show, + 'vote' : usquad_vote, + 'buttons' : usquad_buttons, + 'emote' : usquad_emote, + 'alive' : usquad_alive, +}) +METHOD_LIST = const(METHOD_MAP.keys()) +incoming = None + +def poll_messages(): + global incoming + incoming = radio.receive() + + +# START AND MAIN LOOP +display.show(Image.CHESSBOARD) +usquad_send("bonjour") + +while True: + meas = "" + tags = "" + stamp = 0 + poll_messages() + while incoming is not None: + if incoming.startswith("read_") or incoming.startswith("bonjour"): + incoming = None # skip the message, it comes from another terminal + else: + meas,tags,stamp = ulp_parse(incoming) + incoming = None + execute = True + if("dev_id" in tags.keys() and tags["dev_id"] != DEVID): + execute = False + method = METHOD_MAP.get(meas, None) + if method is None: + execute = False + if execute: + method(tags,stamp) + sleep(100) \ No newline at end of file diff --git a/modules/client/src/main/micropython/ulineprotocol.py b/modules/client-micropython/src/main/micropython/ulineprotocol.py similarity index 76% rename from modules/client/src/main/micropython/ulineprotocol.py rename to modules/client-micropython/src/main/micropython/ulineprotocol.py index 68f9218..2c024ba 100644 --- a/modules/client/src/main/micropython/ulineprotocol.py +++ b/modules/client-micropython/src/main/micropython/ulineprotocol.py @@ -11,7 +11,7 @@ def _pop_head_or_none(arr, peek_only = False): def ulp_parse(msg): meas = None tags = {} - vals = {} + fields = {} tmstp = None frags = msg.split(" ") @@ -28,20 +28,20 @@ def ulp_parse(msg): if frag is not None: if("=" in frag): frag = _pop_head_or_none(frags) - valuesFragment = frag.split(",") - for valueFragment in map(lambda v: v.split("="),valuesFragment): - vals[valueFragment[0]] = valueFragment[1] + fieldsFragment = frag.split(",") + for fieldFragment in map(lambda v: v.split("="),fieldsFragment): + fields[fieldFragment[0]] = fieldFragment[1] frag = _pop_head_or_none(frags) if frag is not None: tmstp = int(frag) - return (meas, tags, vals, tmstp) + return (meas, tags, fields, tmstp) -def ulp_serialize(measurement, tags=None, values=None, timestamp=None): +def ulp_serialize(measurement, tags=None, fields=None, timestamp=None): result = measurement if tags is not None: result += (','.join('{}="{}"'.format(key, value) for key, value in tags.items())) + " " - if values is not None: - result += (','.join('{}={}'.format(key, value) for key, value in values.items())) + " " + if fields is not None: + result += (','.join('{}={}'.format(key, value) for key, value in fields.items())) + " " if timestamp is not None: result += timestamp else: diff --git a/modules/client-mobile/pom.xml b/modules/client-mobile/pom.xml new file mode 100644 index 0000000..b35ff04 --- /dev/null +++ b/modules/client-mobile/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + com.github.cmcrobotics.microsquad + reactor + 0.1.0-SNAPSHOT + ../../pom.xml + + + mobile-client + jar + + Microsquad :: Mobile Client + ${project.name} + + + + UTF-8 + UTF-8 + + + + + + com.github.eirslett + frontend-maven-plugin + 1.7.6 + + ${project.basedir} + ${node.version} + ${npm.version} + + + + install node and npm + + install-node-and-npm + + + + npm install + + npm + + + + npm run build + + npm + + + run build + + + + + + + + diff --git a/modules/client/src/main/micropython/main.py b/modules/client/src/main/micropython/main.py deleted file mode 100644 index 4b9cbb1..0000000 --- a/modules/client/src/main/micropython/main.py +++ /dev/null @@ -1,170 +0,0 @@ -from microbit import display,Image,sleep, button_a, button_b, accelerometer, running_time - -import radio - -SIMU = False -try: - import machine - DEVID = machine.unique_id() -except ImportError: - DEVID = "123456789123456789" - SIMU = True - print("Could not import machine module, DEVICE ID : "+str(DEVID)) - -radio.config(channel=12, group=1) -radio.on() - -IMG_SEND = [(Image.ARROW_N * (i/5)) for i in range(5, -1, -1)] - -def _pop_head_or_none(arr): - if arr and len(arr)>0: - return arr.pop(0) - else: - return None - -def ulp_parse(msg): - meas = None - tags = {} - tmstp = None - - frags = msg.split(" ") - frag = _pop_head_or_none(frags) - if frag is not None: - measFrags = frag.split(",") - if len(measFrags) > 0: - meas = measFrags[0] - if len(measFrags) > 1: - for tagFrag in measFrags[1:]: - tagKV = tagFrag.split("=") - tags[tagKV[0]] = tagKV[1].strip('"\'') - frag = _pop_head_or_none(frags) - if frag is not None: - tmstp = int(frag) - return (meas, tags, tmstp) - -def ulp_serialize(measurement, tags=None, timestamp=None): - result = measurement - if tags is not None: - for key, value in tags.items(): - result += ','+str(key)+"=\""+str(value)+"\"" - if timestamp is not None: - result += " "+str(timestamp) - else: - result += " "+str(running_time()) - return result - -def usquad_send(measurement, tags= None, timestamp=None): - tagz = {"dev_id":DEVID} - if tags is not None: - tagz.update(tags) - msg = ulp_serialize(measurement, tagz, timestamp) - radio.send(msg) - if SIMU == True: - print("Sending : "+msg) - -def usquad_image(tags, timestamp=None): - images_str = tags['value'] - img = [(Image(img_str)) for img_str in images_str.split(";")] - _delay = int(tags.get('delay',50)) - _wait = (tags.get('wait', "true").lower()=="true") - _clear = (tags.get('clear', "true").lower()=="true") - display.show(img, delay=_delay, wait=_wait, clear=_clear) - -def usquad_text(tags, timestamp=None): - text_str = tags['value'].replace("_", " ") - _delay = int(tags.get('delay',50)) - _wait = (tags.get('wait', "true").lower()=="true") - _clear = (tags.get('clear', "true").lower()=="true") - display.show(text_str, delay=_delay, wait=_wait, clear=_clear) - -def usquad_read_accel(tags= None, timestamp=None): - x,y,z = accelerometer.get_values() - usquad_send("read_accel", tags = {"x":x,"y":y,"z":z}) - -def usquad_device_id(tags, timestamp=None): - global DEVID - DEVID = tags.get('id',machine.unique_id()) - -def usquad_vote(tags, timestamp=None): - images_str = tags['value'] - choices = [(Image(img_str)) for img_str in images_str.split(";")] - _max_votes = int(tags.get('votes',1)) - vote_cn = 0 - choices_max = len(choices) - choice = 0 - button_a.get_presses() - button_b.was_pressed() - display.show(choices[choice], delay=50, clear=False,wait=True) - while (vote_cn < _max_votes): - a_presses = button_a.get_presses() - if a_presses > 0: - choice = (choice + a_presses) % choices_max - display.show(choices[choice]) - if button_b.was_pressed(): - usquad_send("read_vote",{"value":choice, "index":vote_cn}) - display.show(IMG_SEND, delay=30, wait=True, clear=True) - vote_cn += 1 - votes_left = _max_votes - vote_cn - if(votes_left > 0): - display.show(str(votes_left), clear=False, wait=True) - sleep(1500) - display.show(choices[choice], clear=False,wait=False) - display.show(Image.TARGET) - -def usquad_buttons(tags = None, timestamp=None): - global incoming - button_a.was_pressed() - button_b.was_pressed() - display.show(Image.TRIANGLE) - stop = False - while not stop: - if button_a.was_pressed(): - usquad_send("read_button_a") - display.show("a") - if button_b.was_pressed(): - usquad_send("read_button_b") - display.show("b") - poll_messages() - if incoming is not None: - stop = True - else: - sleep(200) - display.show(Image.TRIANGLE) - - -usquad_methods = { - 'image' : usquad_image, - 'accel' : usquad_read_accel, - 'text' : usquad_text, - 'vote' : usquad_vote, - 'device_id' : usquad_device_id, - 'buttons' : usquad_buttons -} -incoming = None - - -def poll_messages(): - global incoming - if SIMU == False: - incoming = radio.receive() - if button_a.was_pressed(): - incoming = 'vote,value="99999:99999:99099:99999:99999;99999:55555:00000:55555:99999",duration=4000,votes=4' - -display.show(Image.TARGET) -usquad_send("bonjour") - -while True: - meas = "" - tags = "" - stamp = 0 - - poll_messages() - - while incoming is not None: - meas,tags,stamp = ulp_parse(incoming) - incoming = None - method = usquad_methods.get(meas, None) - if method is not None: - method(tags,stamp) - - sleep(200) \ No newline at end of file diff --git a/modules/gateway/README.md b/modules/gateway/README.md new file mode 100644 index 0000000..9f3df6f --- /dev/null +++ b/modules/gateway/README.md @@ -0,0 +1,26 @@ +# How to initialize the environment + +``` +python3 -m venv usquad-venv +echo "`pwd`/src/main/python `pwd`/src/test/python" > usquad-venv/lib/python3.8/site-packages/gateway.pth +``` + +# How to use + +* Ensure that your MQTT broker is running on port 1883 + +* Execute: +``` +. ./setup-venv.sh +python -m microsquad.gateway.mqtt +``` + +* Follow the instructions given by Bitio to detect your Micro:bit and the gateway should start. + +# How to reset your broker's persistent data (Mosquitto) + +```bash +sudo service mosquitto stop +sudo rm /var/lib/mosquitto/mosquitto.db +sudo service mosquitto start +``` \ No newline at end of file diff --git a/modules/gateway/bin/purge-mosquitto-db.sh b/modules/gateway/bin/purge-mosquitto-db.sh new file mode 100755 index 0000000..468c15c --- /dev/null +++ b/modules/gateway/bin/purge-mosquitto-db.sh @@ -0,0 +1,4 @@ +service mosquitto stop +rm /var/lib/mosquitto/mosquitto.db +service mosquitto start +service mosquitto status \ No newline at end of file diff --git a/modules/gateway/requirements.txt b/modules/gateway/requirements.txt deleted file mode 100644 index 8a9dcad..0000000 --- a/modules/gateway/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -influx_line_protocol>=0.1.4 \ No newline at end of file diff --git a/modules/gateway/setup-venv.sh b/modules/gateway/setup-venv.sh new file mode 100755 index 0000000..9d8c920 --- /dev/null +++ b/modules/gateway/setup-venv.sh @@ -0,0 +1,4 @@ +source usquad-venv/bin/activate +python src/main/python/setup.py install +export PYTHONPATH=`pwd`/src/main/python:`pwd`/src/test/python:$PYTHONPATH + diff --git a/modules/gateway/src/main/python/README.md b/modules/gateway/src/main/python/README.md deleted file mode 100644 index 8c7f7a3..0000000 --- a/modules/gateway/src/main/python/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# How to use - -* Create a virtual environment and activate it - -``` -cd ../../.. -virtualenv usquad-venv -source usquad-venv/bin/activate -cd src/main/python -python gateway.py -``` diff --git a/modules/gateway/src/main/python/microsquad/connector/abstract_connector.py b/modules/gateway/src/main/python/microsquad/connector/abstract_connector.py new file mode 100644 index 0000000..beb9f4a --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/connector/abstract_connector.py @@ -0,0 +1,58 @@ +from abc import ABCMeta,abstractmethod + +import logging +import threading +from microsquad.event import EventType, MicroSquadEvent + +from rx3 import Observable +from rx3.operators import filter + +logger = logging.getLogger(__name__) + +class AbstractConnector(metaclass=ABCMeta): + """ + Thread-based implementation of a connector loop. + """ + + @abstractmethod + def queue(self, message, device_id = None): + """ + Queue a message for radio distribution + """ + pass + + @abstractmethod + def dispatch_next(self): + pass + + def __init__(self, event_source: Observable): + self._thread_terminate = True + self._event_source = event_source + self._event_source.pipe( + filter(lambda e: e.event_type in [EventType.TERMINAL_BROADCAST, EventType.TERMINAL_COMMAND] ) + ).subscribe( + on_next = lambda evt: self.queue(evt.payload, evt.device_id) + ) + + def start(self): + self._thread_terminate = False + self._thread = threading.Thread(target=self._thread_main) + self._thread.daemon = True + self._thread.start() + + def _thread_main(self): + run = True + error_count = 0 + + while run: + try: + self.dispatch_next() + except Exception as e: + error_count += 1 + logging.exception("Error during connector dispatch : {}".format(e)) + + def should_exit(): + return run is False or self._thread_terminate is True + + if should_exit(): + run = False \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/connector/bitio_connector.py b/modules/gateway/src/main/python/microsquad/connector/bitio_connector.py new file mode 100644 index 0000000..5317051 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/connector/bitio_connector.py @@ -0,0 +1,47 @@ +from microbit import display,radio, sleep + +import logging + +from rx3 import Observable + +from queue import Queue,Empty + +from ..mapper.abstract_mapper import AbstractMapper +from .abstract_connector import AbstractConnector + + +class BitioConnector(AbstractConnector): + """ + Simple Bitio connector implementation that uses the radio to receive messages. + It also subscribes to a MicroSquadEvent source to queue up messages to the terminals. + """ + def __init__(self, mapper : AbstractMapper, event_source: Observable): + super().__init__(event_source) + self._queue = Queue(256) + self._mapper = mapper + radio.config(length=64, channel=12, group=12, power=7) + radio.on() + + def queue(self, message, device_id = None): + self._queue.put((message, device_id)) + + def dispatch_next(self): + incoming_msg = radio.receive() + if incoming_msg != "None" and incoming_msg is not None: + # Received message via radio + logging.debug("Receiving "+incoming_msg) + # Map the message to logical device + # TODO: This call should be asynchronous and delegating to a separate scheduler + self._mapper.map_from_microbit(incoming_msg) + + try: + outgoing_msg = self._queue.get_nowait() + payload = outgoing_msg[0] + device_id = outgoing_msg[1] + if(device_id is not None): + payload += ",dev_id={}".format(str(device_id)) + logging.debug("Sending " + payload+" (left "+str((self._queue.qsize()))+")") + radio.send(payload) + except Empty: + pass + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/connector/dummy_connector.py b/modules/gateway/src/main/python/microsquad/connector/dummy_connector.py new file mode 100644 index 0000000..2ab406b --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/connector/dummy_connector.py @@ -0,0 +1,46 @@ +import logging + +from ..mapper.abstract_mapper import AbstractMapper +from .abstract_connector import AbstractConnector + +from rx3 import Observable +from queue import SimpleQueue, Empty + +class DummyConnector(AbstractConnector): + """ + Simple dummy connector implementation that receives messages via a method and queues them up. + It then forwards the queued message to the mapper (FIFO), when asked to dispatch one. + """ + def __init__(self, mapper : AbstractMapper, event_source : Observable): + super().__init__(event_source) + self._incoming_queue = SimpleQueue() + self._mapper = mapper + self.__last_sent = None + self.__last_sent_device = None + + def queue(self, message, device_id = None): + if device_id is None: + print("'Sending' Message to Microbits ;-) : {}".format(message)) + else: + print("'Sending' Message to Microbit Device {} ;-) : {}".format(str(device_id), message)) + self.__last_sent_device = str(device_id) + self.__last_sent = message + + def simulate_message(self,msg : str): + """ + Simulate a message coming from one of the microbits + """ + self._incoming_queue.put(msg) + + def dispatch_next(self): + try: + next_incoming_message = self._incoming_queue.get_nowait() + self._mapper.map_from_microbit(next_incoming_message) + except Empty: + pass + + @property + def last_sent(self): + return self.__last_sent + + diff --git a/modules/gateway/src/main/python/microsquad/controller/homie/README.md b/modules/gateway/src/main/python/microsquad/controller/homie/README.md new file mode 100644 index 0000000..d939477 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/controller/homie/README.md @@ -0,0 +1,22 @@ +# Controller for Homie devices + +A Microsquad controller obtains MQTT events and performs callbacks for potential subscribers. +Also allows to update settable properties. + +Callbacks are handled by RxPy observables. + +## Supported reactive event types + +* terminal_discovered +* player_discovered +* game_discovered +* terminal property update : vote,accel,button,temperature (c.f. Microsquad event types) + +## Supported settable property updates + +* update_terminal_property(terminal_id,property_name,property_value) +* update_gateway_property(gateway_id,property_name,property_value) +* update_player_property(player_id,property_name,property_value) + + + diff --git a/modules/gateway/src/main/python/microsquad/controller/homie/homie_controller.py b/modules/gateway/src/main/python/microsquad/controller/homie/homie_controller.py new file mode 100644 index 0000000..f1363ee --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/controller/homie/homie_controller.py @@ -0,0 +1,77 @@ + +import logging +from homieclient import HomieClient + +from rx3 import Observable + +from ...event import MicroSquadEvent,EventType + +logger = logging.getLogger(__name__) + + +class HomieController(): + + + """ + A controller that relies on homieclient to obtain and cache property updates, as well + as issue callbacks on discovery events. + """ + def __init__(self,mqtt_settings,homie_settings, event_source : Observable = None) -> None: + self.mqtt_settings = mqtt_settings + self.homie_settings = {"HOMIE_PREFIX":"homie/"} + self.homie_settings.update(homie_settings) + self.homie_client = None + self.mqtt_transport = "tcp" + self.event_source = event_source + self._known_terminals = [] + self._known_games = [] + self._known_players = [] + + + def connect(self): + logger.debug( + "MQTT Connecting to {} as client {}".format( + self.mqtt_settings["MQTT_BROKER"], self.mqtt_settings["MQTT_CLIENT_ID"] + ) + ) + """ + HomieClient limitations : + * No Websockets support (WS_PATH, MQTT_TRANSPORT, TLS SUPPORT) + * Read-only : does not support updating properties + * Does not use asyncio futures + """ + self.homie_client = HomieClient(server=self.mqtt_settings["MQTT_BROKER"], prefix=self.homie_settings["HOMIE_PREFIX"]) + self.homie_client._on_property_updated = self._on_property_updated + + self.homie_client.connect() + + def property_updated(self,node, property, value:str): + # Check if it's a terminal device + # If so, issue callbacks and rxpy events + if(node.device.id.startswith("terminal-") and node.device.id not in self._known_terminals): + self._known_terminals.append(node.device.id) + # Terminal event + if(self.event_source is not None): + logger.debug(f"New terminal detected : {node.device['device-id']}") + self.event_source.on_next(MicroSquadEvent(EventType.TERMINAL_DISCOVERED,node.device["device-id"])) + # Forward the event to any RxPy observers + self.event_source.on_next(MicroSquadEvent(EventType[str(node.name+"_"+property)],node.device["device-id"],value)) + + if(node.name.startswith("game") ): + if(property == "audience-code" and value not in self._known_games): + self._known_games.append(value) + # Terminal event + if(self.event_source is not None): + logger.debug(f"New game started : {value}") + self.event_source.on_next(MicroSquadEvent(EventType.GAME_DISCOVERED,payload=value)) + # TODO : Reset all known players ? all known terminals ? + # TODO: Forward the property update to any listeners + + if(node.name.startswith("player-") ): + if(property == "terminal-id" and value not in self._known_players): + self._known_games.append(value) + if(self.event_source is not None): + logger.debug(f"New player discovered : {value}") + self.event_source.on_next(MicroSquadEvent(EventType.PLAYER_DISCOVERED,payload=value)) + # TODO: Forward the property update to any listeners + diff --git a/modules/gateway/src/main/python/microsquad/controller/homie/simple_homie_controller.py b/modules/gateway/src/main/python/microsquad/controller/homie/simple_homie_controller.py new file mode 100644 index 0000000..a602199 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/controller/homie/simple_homie_controller.py @@ -0,0 +1,134 @@ + +import logging +import paho.mqtt.client as mqtt_client +import asyncio +import threading +import functools + +logger = logging.getLogger(__name__) + + +class SimpleHomieController(): + """ + A simple controller without dependencies (besides an MQTT client), relies on Homie + convention to collect device / node / property updates. + """ + def __init__(self,mqtt_settings,homie_settings) -> None: + self.mqtt_settings = mqtt_settings + self.mqtt_client = None + self.mqtt_transport = "tcp" + self.mqtt_protocol = mqtt_client.MQTTv311 + + def connect(self): + logger.debug( + "MQTT Connecting to {} as client {}".format( + self.mqtt_settings["MQTT_BROKER"], self.mqtt_settings["MQTT_CLIENT_ID"] + ) + ) + + if self.mqtt_settings["MQTT_PROTOCOL"]: + if(self.mqtt_settings["MQTT_PROTOCOL"] in [mqtt_client.MQTTv31, mqtt_client.MQTTv311, mqtt_client.MQTTv5]): + self.mqtt_protocol = self.mqtt_settings["MQTT_PROTOCOL"] + else: + logger.info("MQTT protocol {} unsupported ".format(self.mqtt_settings["MQTT_PROTOCOL"])) + + if self.mqtt_settings["MQTT_TRANSPORT"]: + self.mqtt_transport = self.mqtt_settings["MQTT_TRANSPORT"] + + if self.mqtt_settings["MQTT_WS_PATH"]: + self.mqtt_transport = "websockets" + + self.mqtt_client = mqtt_client.Client(client_id=self.mqtt_settings["MQTT_CLIENT_ID"], transport=self.mqtt_transport, protocol=self.mqtt_protocol) + self.mqtt_connected = False + self.mqtt_client.on_connect = self._on_connect + self.mqtt_client.on_message = self._on_message + self.mqtt_client.on_disconnect = self._on_disconnect + + if self.mqtt_settings["MQTT_USERNAME"]: + self.mqtt_client.username_pw_set( + self.mqtt_settings["MQTT_USERNAME"], + password=self.mqtt_settings["MQTT_PASSWORD"], + ) + + if self.mqtt_settings["MQTT_WS_PATH"]: + self.mqtt_client.ws_set_options(path=self.mqtt_settings["MQTT_WS_PATH"]) + + if self.mqtt_settings["MQTT_USE_TLS"]: + self.mqtt_client.tls_set() + + try: + self.mqtt_client.connect( + self.mqtt_settings["MQTT_BROKER"], + port=self.mqtt_settings["MQTT_PORT"], + keepalive=self.mqtt_settings["MQTT_KEEPALIVE"], + ) + self.mqtt_client.loop_start() + except Exception as e: + logger.warning("Homie Controller MQTT client unable to connect to Broker {}".format(e)) + + + def start(): + try: + asyncio.set_event_loop(self.event_loop) + logger.info ('Starting Homie Controller asyincio publish loop forever') + self.event_loop.run_forever() + logger.warning ('Homie Controller Event publish loop stopped') + except Exception as e: + logger.error ('Error in Homie Controller event loop {}'.format(e)) + + self.event_loop = asyncio.new_event_loop() + + logger.info("Starting Homie Controller MQTT publish thread") + self._ws_thread = threading.Thread(target=start, args=()) + + self._ws_thread.daemon = True + self._ws_thread.start() + + def publish(self, topic, payload, retain, qos): + logger.debug( + "MQTT publish topic: {}, payload: {}, retain {}, qos {}".format( + topic, payload, retain, qos + ) + ) + def publish(): + self.mqtt_client.publish(topic, payload, retain=retain, qos=qos) + + self.event_loop.call_soon_threadsafe(functools.partial(publish)) + + def _on_connect(self, client, userdata, flags, rc): + if rc > 0: + rc_text = mqtt_client.connack_string(rc) + logger.fatal("Homie Controller MQTT - connection: Result code {} {}, Flags {}".format(rc, rc_text,flags)) + else: + logger.debug("Homie Controller MQTT - connection successful : Result code {}, Flags {}".format(rc, flags)) + + + # TODO : Subscribe to device / node / property patterns under given Homie prefix + + # + ########### + + self.mqtt_connected = rc == 0 + + def _on_message(self, client, userdata, msg): + topic = msg.topic + payload = msg.payload.decode("utf-8") + + # Split the topic into device / node / property and update the corresponding in-memory cache + + + # Invoke matching handlers + # * on_new_terminal (known terminal ?) + # * on_new_player (known player ?) + # * on_new_game (known game ?) + # * on_update_terminal_property (validate terminal name and node ?) + + + def _on_disconnect(self, client, userdata, rc): + self.mqtt_connected = False + if rc > 0: + rc_text = mqtt_client.error_string(rc) + + logger.warning( + "Homie Controller MQTT - unexpected disconnection {} {} Result Code : {} {}".format(client, userdata, rc, rc_text) + ) \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/event.py b/modules/gateway/src/main/python/microsquad/event.py new file mode 100644 index 0000000..486f902 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/event.py @@ -0,0 +1,49 @@ + +import enum + + +@enum.unique +class EventType(enum.Enum): + BONJOUR = "bonjour" + VOTE = "vote" + ACCELERATOR = "accel" + BUTTON = "button" + TEMPERATURE = "temperature" + TERMINAL_BROADCAST = "terminal_broadcast" + TERMINAL_COMMAND = "terminal_command" + TERMINAL_DISCOVERED = "terminal_discovered" + GAME_DISCOVERED = "game_discovered" + PLAYER_DISCOVERED = "player_discovered" + + GAME_START = "game_start" + GAME_PAUSE = "game_pause" + GAME_STOP = "game_stop" + GAME_TRANSITION = "game_transition" + GAME_TRANSITIONS_UPDATED = "game_transitions_updated" + + + def equals(self, string): + return self.value == string + +EVENTS_GAME = [EventType.GAME_START,EventType.GAME_STOP, EventType.GAME_TRANSITION, EventType.GAME_TRANSITIONS_UPDATED] +EVENTS_SENSOR = [EventType.BONJOUR,EventType.VOTE,EventType.ACCELERATOR,EventType.BUTTON,EventType.TEMPERATURE] +EVENTS_TERMINAL = [EventType.TERMINAL_BROADCAST, EventType.TERMINAL_COMMAND] + + +class MicroSquadEvent(): + def __init__(self, event_type:EventType, device_id=None, payload = None ) -> None: + self.__event_type = event_type + self.__payload = payload + self.__device_id = device_id + + @property + def event_type(self): + return self.__event_type + + @property + def payload(self): + return self.__payload + + @property + def device_id(self): + return self.__device_id \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/game/abstract_game.py b/modules/gateway/src/main/python/microsquad/game/abstract_game.py new file mode 100644 index 0000000..0d3b05d --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/abstract_game.py @@ -0,0 +1,87 @@ +from abc import ABCMeta,abstractmethod + +import logging +import threading +import enum + +from homie.node.property.property_base import Property_Base +from microsquad.event import EventType, MicroSquadEvent +from microsquad.mapper.homie.gateway.node_player import NodePlayer + +from ..mapper.homie.gateway.device_gateway import DeviceGateway +from rx3 import Observable + +logger = logging.getLogger(__name__) + + +class AGame(metaclass=ABCMeta): + """ + Base class for MicroSquad games + """ + + def __init__(self, event_source: Observable, gateway : DeviceGateway) -> None: + self._event_source = event_source + self._device_gateway = gateway + self._available_transitions : list = [] + self._last_fired_transition = None + + @abstractmethod + def process_event(self, event:MicroSquadEvent) -> None: + """ + Handle the next game event + """ + pass + + @abstractmethod + def start(self) -> None: + pass + + def stop(self) -> None: + logger.debug("Game {} now stopped.".format(__name__)) + + def fire_transition(self, transition) -> None: + self._last_fired_transition = transition + + @property + def last_fired_transition(self): + return self._last_fired_transition + + @property + def event_source(self): + return self._event_source + + @property + def device_gateway(self): + return self._device_gateway + + def get_all_player_nodes(self) -> list: + return filter(lambda node : node.id.startswith("player-"), self._device_gateway.nodes.values()) + + def get_player_node_by_id(self, id:str) -> NodePlayer : + return self._device_gateway.get_node("player-{}".format(id)) + + def get_available_transitions_as_strings(self) -> list: + return [t.value for t in self._available_transitions] + + def update_available_transitions(self,transitions:list) -> None: + # TODO Add transition validation and/or transformation to JSON format + self._available_transitions = transitions + self.event_source.on_next(MicroSquadEvent(EventType.GAME_TRANSITIONS_UPDATED,payload=[t.value for t in self._available_transitions])) + +def set_next_in_collection(property: Property_Base, collection) -> None: + idx = 0 + current_value = property.value + if(current_value in collection): + idx = collection.index(current_value) +1 + if(idx >= len(collection)) : + idx = 0 + property.value = collection[idx] + +def set_prev_in_collection(property: Property_Base, collection) -> None: + idx = 0 + current_value = property.value + if(current_value in collection): + idx = collection.index(current_value) -1 + if(idx < 0) : + idx = len(collection)-1 + property.value = collection[idx] diff --git a/modules/gateway/src/main/python/microsquad/game/alice/alice.py b/modules/gateway/src/main/python/microsquad/game/alice/alice.py new file mode 100644 index 0000000..8bd9f02 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/alice/alice.py @@ -0,0 +1,54 @@ +import logging + +from ..abstract_game import AGame, set_next_in_collection, set_prev_in_collection + +import enum +from rx3 import Observable +from microsquad.event import EVENTS_SENSOR, EventType, MicroSquadEvent +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway + + +logger = logging.getLogger(__name__) + +@enum.unique +class TRANSITIONS(enum.Enum): + SIZE = "Size" + ROTATE = "Rotate" + def equals(self, string): + return self.value == string + +class Game(AGame): + """ + A simple game that allows to declare new players and customize their appearance + """ + def __init__(self, event_source: Observable, gateway : DeviceGateway) -> None: + super().__init__(event_source, gateway) + + + def start(self) -> None: + print("Alice game starting") + super().update_available_transitions(list(TRANSITIONS)) + super().fire_transition(TRANSITIONS.SIZE) + super().device_gateway.update_broadcast("buttons") + + def fire_transition(self, transition) -> None: + super().fire_transition(transition) + + def process_event(self, event:MicroSquadEvent) -> None: + logger.debug("Alice game received event {} for device {}: {}".format(event.event_type.name, event.device_id, event.payload)) + self.device_gateway.get_node("players-manager").add_player(event.device_id) + if event.event_type == EventType.BUTTON: + transition =TRANSITIONS(self._last_fired_transition) + if transition == TRANSITIONS.SIZE: + if(event.event_type == EventType.BUTTON): + factor = 0.9 + if event.payload["button"]=="b" : + factor = 1.1 + self.device_gateway.get_node("player-"+event.device_id).get_property("scale").value *= factor + if transition == TRANSITIONS.ROTATE: + angle_modifier = -0.2 + if event.payload["button"]=="b" : + angle_modifier = 0.2 + self.device_gateway.get_node("player-"+event.device_id).get_property("rotation").value += angle_modifier + + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/game/antimatter-summary.png b/modules/gateway/src/main/python/microsquad/game/antimatter-summary.png new file mode 100644 index 0000000..35f13f1 Binary files /dev/null and b/modules/gateway/src/main/python/microsquad/game/antimatter-summary.png differ diff --git a/modules/gateway/src/main/python/microsquad/game/antimatter/antimatter.py b/modules/gateway/src/main/python/microsquad/game/antimatter/antimatter.py new file mode 100644 index 0000000..9e3a852 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/antimatter/antimatter.py @@ -0,0 +1,19 @@ +from microsquad.game.particle_voting_game import AParticleVotingGame +import base64 +import random +from rx3 import Observable +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway + +from ..particles import PARTICLE, PARTICLES + +class Game(AParticleVotingGame): + def __init__(self, event_source: Observable, gateway : DeviceGateway) -> None: + super().__init__(event_source, gateway, PARTICLE.ELECTRON, PARTICLE.POSITRON, 2) + with open("src/main/python/microsquad/game/antimatter-summary.png", "rb") as image_file: + string_data = base64.b64encode(image_file.read()) + super().device_gateway.get_node("scoreboard").get_property("image").value = "data:image/png;base64,"+(string_data.decode("ascii")) + + + def get_random_particle(self): + return random.choice([PARTICLE.ELECTRON,PARTICLE.POSITRON]) + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/game/charge-electron.png b/modules/gateway/src/main/python/microsquad/game/charge-electron.png new file mode 100644 index 0000000..75c0c0c Binary files /dev/null and b/modules/gateway/src/main/python/microsquad/game/charge-electron.png differ diff --git a/modules/gateway/src/main/python/microsquad/game/charges-summary.png b/modules/gateway/src/main/python/microsquad/game/charges-summary.png new file mode 100644 index 0000000..8375db3 Binary files /dev/null and b/modules/gateway/src/main/python/microsquad/game/charges-summary.png differ diff --git a/modules/gateway/src/main/python/microsquad/game/charges/charges.py b/modules/gateway/src/main/python/microsquad/game/charges/charges.py new file mode 100644 index 0000000..ef420d2 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/charges/charges.py @@ -0,0 +1,16 @@ +from microsquad.game.particle_voting_game import AParticleVotingGame + +import random +import base64 +from rx3 import Observable +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway + +from ..particles import PARTICLE, PARTICLES + +class Game(AParticleVotingGame): + def __init__(self, event_source: Observable, gateway : DeviceGateway) -> None: + super().__init__(event_source, gateway, PARTICLE.ELECTRON, PARTICLE.PROTON, 0) + with open("src/main/python/microsquad/game/charge-electron.png", "rb") as image_file: + string_data = base64.b64encode(image_file.read()) + super().device_gateway.get_node("scoreboard").get_property("image").value = "data:image/png;base64,"+(string_data.decode("ascii")) + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/game/customeeze/customeeze.py b/modules/gateway/src/main/python/microsquad/game/customeeze/customeeze.py new file mode 100644 index 0000000..77836a3 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/customeeze/customeeze.py @@ -0,0 +1,121 @@ +from rx3 import Observable +from microsquad.event import EVENTS_SENSOR, EventType, MicroSquadEvent +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway +import enum +import logging +import base64 + +from ..abstract_game import AGame, set_next_in_collection, set_prev_in_collection +from ..emotes import find_emote_by_idx + +SKINS = [ + "alienA","alienB","animalA","animalB","animalBaseA","animalBaseB","animalBaseC","animalBaseD","animalBaseE","animalBaseF" + ,"animalBaseG","animalBaseH","animalBaseI","animalBaseJ","animalC","animalD","animalE","animalF","animalG","animalH","animalI" + ,"animalJ","astroFemaleA","astroFemaleB","astroMaleA","astroMaleB" + ,"athleteFemaleBlue","athleteFemaleGreen","athleteFemaleRed","athleteFemaleYellow","athleteMaleBlue","athleteMaleGreen" + ,"athleteMaleRed","athleteMaleYellow" + ,"businessMaleA","businessMaleB" + ,"casualFemaleA","casualFemaleB","casualMaleA","casualMaleB","cyborg" + ,"fantasyFemaleA","fantasyFemaleB","fantasyMaleA","fantasyMaleB","farmerA","farmerB" + ,"militaryFemaleA","militaryFemaleB","militaryMaleA","militaryMaleB" + ,"racerBlueFemale","racerBlueMale","racerGreenFemale","racerGreenMale","racerOrangeFemale","racerOrangeMale" + ,"racerPurpleFemale","racerPurpleMale","racerRedFemale","racerRedMale","robot","robot2","robot3" + ,"survivorFemaleA","survivorFemaleB","survivorMaleA","survivorMaleB","zombieA","zombieB","zombieC" +] + +ATTITUDES = ["Idle","Run","Walk","CrouchWalk","Wave"] + + + +@enum.unique +class TRANSITIONS(enum.Enum): + SELECT_SKIN = "Select skin" + SELECT_ATTITUDE = "Select attitude" + EMOJIS = "Emojis" + CLEAR = "Clear" + def equals(self, string): + return self.value == string + +TRANSITION_GRAPH = { + TRANSITIONS.SELECT_SKIN : [TRANSITIONS.SELECT_ATTITUDE], + TRANSITIONS.SELECT_ATTITUDE : [TRANSITIONS.EMOJIS], + TRANSITIONS.EMOJIS : [TRANSITIONS.EMOJIS, TRANSITIONS.CLEAR] + } + + + +logger = logging.getLogger(__name__) + +class Game(AGame): + """ + A simple game that allows to declare new players and customize their appearance + """ + def __init__(self, event_source: Observable, gateway : DeviceGateway) -> None: + super().__init__(event_source, gateway) + + def start(self) -> None: + print("Customeeze starting") + super().update_available_transitions([TRANSITIONS.SELECT_SKIN]) + super().device_gateway.update_broadcast("buttons") + super().device_gateway.get_node("scoreboard").get_property("image").value = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAAoCAYAAAAi24Q0AAAAq0lEQVR42u3WQQEAAAQEsJNcdEr42VKskukAAHCmBAsAQLAAAAQLAECwAAAQLAAAwQIAECwAAAQLAECwAAAECwAAwQIAECwAAMECABAsAAAECwBAsAAABAsAAMECABAsAADBAgBAsAAABAsAQLAAABAsAADBAgAQLAAAwQIAQLAAAAQLAECwAAAQLAAAwQIAECwAAAQLAECwAAAECwAAwQIAECwAAMECAPhnAVXnO9nPYEM/AAAAAElFTkSuQmCC" + + def process_event(self, event:MicroSquadEvent) -> None: + logger.debug("Customeeze received event {} for device {}: {}".format(event.event_type.name, event.device_id, event.payload)) + self.device_gateway.get_node("players-manager").add_player(event.device_id) + if event.event_type in EVENTS_SENSOR: + playerNode = self.device_gateway.get_node("player-"+event.device_id) + if playerNode is None: + logger.warn("Player {} is not known".format("player-"+event.device_id)) + else: + if super().last_fired_transition is None: + playerNode.get_property("animation").value = "Wave" + else: + last_fired = TRANSITIONS(super().last_fired_transition) + if last_fired == TRANSITIONS.SELECT_SKIN: + if event.payload["button"]=="a" : + # Shift the player's skin + set_next_in_collection(playerNode.get_property("skin"), SKINS) + elif event.payload["button"]=="b" : + # Shift the player's skin + set_prev_in_collection(playerNode.get_property("skin"), SKINS) + elif last_fired == TRANSITIONS.SELECT_ATTITUDE: + if event.payload["button"]=="a" : + set_next_in_collection(playerNode.get_property("animation"), ATTITUDES) + elif event.payload["button"]=="b" : + set_prev_in_collection(playerNode.get_property("animation"), ATTITUDES) + elif last_fired == TRANSITIONS.EMOJIS: + if event.event_type == EventType.VOTE: + emote = find_emote_by_idx(int(event.payload["value"])) + if emote is not None: + playerNode.get_property("say").value = "{} !".format(emote.entity) + + + + + def fire_transition(self, transition) -> None: + super().fire_transition(transition) + # Obtain the next transitions in the graph + # If none, the game can be stopped + next_transitions = TRANSITION_GRAPH.get(TRANSITIONS(self._last_fired_transition), None) + + if(next_transitions is not None and len(next_transitions) > 0): + super().update_available_transitions(next_transitions) + else: + super().update_available_transitions([]) + + last_fired = TRANSITIONS(self._last_fired_transition) + if( last_fired == TRANSITIONS.EMOJIS): + # Switch everybody back to idle + # Trigger a vote + super().device_gateway.update_broadcast("emote,v=5") + elif(last_fired == TRANSITIONS.CLEAR): + for pn in self.get_all_player_nodes(): + pn.get_property("say-duration").value = 60000 + pn.get_property("say").value = "" + pn.get_property("animation").value = "Idle" + pn.get_property("scale").value = 1.0 + + + def stop(self) -> None: + print("Customeeze stopped") + diff --git a/modules/gateway/src/main/python/microsquad/game/emotes.py b/modules/gateway/src/main/python/microsquad/game/emotes.py new file mode 100644 index 0000000..050664f --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/emotes.py @@ -0,0 +1,24 @@ +import enum + +@enum.unique +class EMOTE(enum.Enum): + HEART = ("heart",0, "❤") + SAD = ("sad",1, "😞") + HAPPY = ("happy",2, "😀") + SKULL = ("skull",3, "😵") + + def __init__(self, id:str,idx:int, entity:str) -> None: + self.id = id + self.idx = idx + self.entity = entity + + def equals(self, string) -> bool: + return self.value == string + +def find_emote_by_idx(idx:int) -> EMOTE: + return next((emote for emote in list(EMOTE) if emote.idx == idx), None) + +def find_emote_by_ide(id:str) -> EMOTE: + return next((emote for emote in list(EMOTE) if emote.id == id), None) + +EMOTES = list(EMOTE) \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/game/game_manager.py b/modules/gateway/src/main/python/microsquad/game/game_manager.py new file mode 100644 index 0000000..ac8f862 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/game_manager.py @@ -0,0 +1,71 @@ + +import logging +import threading +from microsquad.event import EventType, MicroSquadEvent, EVENTS_GAME, EVENTS_SENSOR + +from rx3 import Observable +from rx3.operators import filter + +import importlib + + +from microsquad.game.abstract_game import AGame +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway + +logger = logging.getLogger(__name__) + +class GameManager(): + """ + A Game Manager instantiates and controls the lifecycle of games. It also propagate relevant + events (e.g. from terminal sensors) to the currently running game. + """ + + def __init__(self, event_source: Observable, device_gateway: DeviceGateway): + self._thread_terminate = True + self._event_source = event_source + self._device_gateway = device_gateway + self._event_source.pipe( + filter(lambda e: e.event_type in EVENTS_GAME) + ).subscribe( + on_next = self.handle_game_events + ) + + self._current_game : AGame = None + + self._event_source.pipe( + filter(lambda e: e.event_type in EVENTS_SENSOR) + ).subscribe( + on_next = self.forward_sensor_events + ) + + def handle_game_events(self, event:MicroSquadEvent) -> None: + if(event.event_type == EventType.GAME_START): + # Locate the Game in the microsquad.game module and connect it to the event source + # The game is expected under microsquad.game...Game + GameClass = getattr(importlib.import_module("microsquad.game."+event.payload+"."+event.payload), "Game") + # Instantiate the class (pass arguments to the constructor, if needed) + self._current_game = GameClass(self._event_source, self._device_gateway) + self._current_game.start() + self._device_gateway.game_node.get_property("game-status").value = "RUNNING" + elif(event.event_type == EventType.GAME_STOP): + if(self._current_game is not None): + self._current_game.stop() + self._device_gateway.game_node.get_property("game-status").value = "STOPPED" + self._current_game = None + elif(event.event_type == EventType.GAME_TRANSITION): + if(self._current_game is not None): + if(event.payload in self._current_game.get_available_transitions_as_strings()): + self._current_game.fire_transition(event.payload) + elif(event.event_type == EventType.GAME_TRANSITIONS_UPDATED): + if(self._current_game is not None and event.payload is not None): + self._device_gateway.game_node.get_property("transitions").value = ",".join(event.payload) + + def forward_sensor_events(self, event:MicroSquadEvent) -> None: + if(self._current_game is not None): + self._current_game.process_event(event) + + + @property + def current_game(self): + return self._current_game + diff --git a/modules/gateway/src/main/python/microsquad/game/mass-summary.png b/modules/gateway/src/main/python/microsquad/game/mass-summary.png new file mode 100644 index 0000000..5f19f80 Binary files /dev/null and b/modules/gateway/src/main/python/microsquad/game/mass-summary.png differ diff --git a/modules/gateway/src/main/python/microsquad/game/mass/mass.py b/modules/gateway/src/main/python/microsquad/game/mass/mass.py new file mode 100644 index 0000000..cdf4be5 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/mass/mass.py @@ -0,0 +1,16 @@ +from microsquad.game.particle_voting_game import AParticleVotingGame +import base64 +import random +from rx3 import Observable +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway + +from ..particles import PARTICLE, PARTICLES + +class Game(AParticleVotingGame): + def __init__(self, event_source: Observable, gateway : DeviceGateway) -> None: + super().__init__(event_source, gateway, PARTICLE.PHOTON, PARTICLE.NEUTRON, 1) + with open("src/main/python/microsquad/game/mass-summary.png", "rb") as image_file: + string_data = base64.b64encode(image_file.read()) + super().device_gateway.get_node("scoreboard").get_property("image").value = "data:image/png;base64,"+(string_data.decode("ascii")) + + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/game/mystere/mystere.py b/modules/gateway/src/main/python/microsquad/game/mystere/mystere.py new file mode 100644 index 0000000..539dec2 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/mystere/mystere.py @@ -0,0 +1,148 @@ +import logging + +from microsquad.mapper.homie.gateway.node_player import NodePlayer +from microsquad.mapper.homie.terminal.device_terminal import DeviceTerminal + +from ..abstract_game import AGame, set_next_in_collection, set_prev_in_collection + +import enum +import random +import base64 + +from rx3 import Observable +from microsquad.event import EVENTS_SENSOR, EventType, MicroSquadEvent +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway + +from ..particles import PARTICLE, PARTICLES + + +@enum.unique +class TRANSITIONS(enum.Enum): + START = "Start" + SEND_PARTICLE = "Send particle" + RESEND = "Resend" + VOTE = "Vote" + RESULTS = "Show results" + END = "End Game" + CLEAR = "Clear" + RESET = "Reset" + def equals(self, string): + return self.value == string + +TRANSITION_GRAPH = { + TRANSITIONS.START : [TRANSITIONS.START,TRANSITIONS.SEND_PARTICLE], + TRANSITIONS.SEND_PARTICLE : [TRANSITIONS.RESEND,TRANSITIONS.VOTE], + TRANSITIONS.RESEND : [TRANSITIONS.RESEND,TRANSITIONS.VOTE], + TRANSITIONS.VOTE : [TRANSITIONS.VOTE,TRANSITIONS.RESULTS], + TRANSITIONS.RESULTS : [TRANSITIONS.SEND_PARTICLE, TRANSITIONS.END], + TRANSITIONS.END : [TRANSITIONS.RESET,TRANSITIONS.CLEAR] + } + +logger = logging.getLogger(__name__) + + +class Game(AGame): + def __init__(self, event_source: Observable, gateway : DeviceGateway) -> None: + super().__init__(event_source, gateway) + self._last_sent_particle = None + self._votes = {} + self._scores = {} + + def start(self) -> None: + print("Mystery Particles game starting") + super().update_available_transitions([TRANSITIONS.SEND_PARTICLE]) + self.fire_transition(TRANSITIONS.START) + with open("src/main/python/microsquad/game/summary.png", "rb") as image_file: + string_data = base64.b64encode(image_file.read()) + super().device_gateway.get_node("scoreboard").get_property("image").value = "data:image/png;base64,"+(string_data.decode("ascii")) + + def process_event(self, event:MicroSquadEvent) -> None: + logger.debug("Game received event {} for device {}: {}".format(event.event_type.name, event.device_id, event.payload)) + self.device_gateway.get_node("players-manager").add_player(event.device_id) + playerNode = self.device_gateway.get_node("player-"+event.device_id) + say_dur= playerNode.get_property("say-duration").value + if say_dur is None or say_dur < 300000: + playerNode.get_property("say-duration").value = 300000 + if event.event_type in EVENTS_SENSOR: + if self._last_fired_transition == TRANSITIONS.START.value: + if event.event_type == EventType.BUTTON: + playerNode.get_property("animation").value = "Wave" + if self._last_fired_transition == TRANSITIONS.VOTE.value: + if event.event_type == EventType.VOTE: + # Store the player's vote + self._votes[event.device_id] = int(event.payload["value"]) + playerNode.get_property("say").value = "🎁" + + + def fire_transition(self, transition) -> None: + super().fire_transition(transition) + # Obtain the next transitions in the graph + # If none, the game can be stopped + next_transitions = TRANSITION_GRAPH.get(TRANSITIONS(self._last_fired_transition), None) + + + if(next_transitions is not None and len(next_transitions) > 0): + super().update_available_transitions(next_transitions) + else: + super().update_available_transitions([]) + + last_fired = TRANSITIONS(self._last_fired_transition) + if(last_fired == TRANSITIONS.START): + super().device_gateway.update_broadcast("buttons") + elif(last_fired == TRANSITIONS.SEND_PARTICLE): + self._votes = {} + for player_node in self.get_all_player_nodes(): + player_node.get_property("animation").value = "Idle" + player_node.get_property("say").value = "" + self._last_sent_particle = random.choice(PARTICLES) + logger.debug("Sending {}".format(self._last_sent_particle.identifier)) + + # Split the detectors into three categories, send them different visualizations + self._dispatch_visualizations(self._last_sent_particle) + + elif(last_fired == TRANSITIONS.RESEND): + logger.debug("Re-Sending {}".format(self._last_sent_particle.identifier)) + self._dispatch_visualizations(self._last_sent_particle) + elif(last_fired == TRANSITIONS.VOTE): + super().device_gateway.update_broadcast("vote,v=3") + elif(last_fired == TRANSITIONS.RESULTS): + # Tally up the votes, make players say the result, change their animation (DEATH if they are wrong) + for player_id,vote_value in self._votes.items(): + player_node = self.get_player_node_by_id(player_id) + if(vote_value == self._last_sent_particle.idx): + # Correct vote ! + player_node.get_property("say").value = "" + player_node.get_property("animation").value = "Idle" + player_node.get_property("scale").value *= 1.1 + if player_id not in self._scores.keys(): + self._scores[player_id] = 0 + self._scores[player_id] += 1 + else: + _defeat(player_node, "❌") + for player_node in self.get_all_player_nodes(): + if(player_node.get_property("terminal-id").value not in self._votes.keys()): + _defeat(player_node, "❓") + elif(last_fired == TRANSITIONS.CLEAR): + for player_node in self.get_all_player_nodes(): + player_node.get_property("say").value = "" + player_node.get_property("animation").value = "Idle" + elif(last_fired == TRANSITIONS.RESET): + for player_node in self.get_all_player_nodes(): + player_node.get_property("say").value = "" + player_node.get_property("animation").value = "Idle" + player_node.get_property("scale").value = 1.0 + + def _dispatch_visualizations(self,sent_particle:PARTICLE): + for index,player_node in enumerate(super().get_all_player_nodes()): + terminal_id = player_node.get_property("terminal-id").value + terminal_node: DeviceTerminal = super().device_gateway.terminals[terminal_id] + terminal_node.update_command("show,p={},v={}".format(sent_particle.idx,index%3)) + + def stop(self) -> None: + print("Mystere stopped") + + +def _defeat(player_node:NodePlayer,emoji_entity:str): + player_node.get_property("say").value = "{}".format(emoji_entity) + player_node.get_property("animation").value = "Death" + player_node.get_property("scale").value *= 0.9 \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/game/particle_voting_game.py b/modules/gateway/src/main/python/microsquad/game/particle_voting_game.py new file mode 100644 index 0000000..4248482 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/particle_voting_game.py @@ -0,0 +1,133 @@ +from abc import ABCMeta,abstractmethod + +import logging + +from microsquad.mapper.homie.gateway.node_player import NodePlayer + +from .abstract_game import AGame, set_next_in_collection, set_prev_in_collection + +import enum +import random +from rx3 import Observable +from microsquad.event import EVENTS_SENSOR, EventType, MicroSquadEvent +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway + +from .particles import PARTICLE, PARTICLES + + +@enum.unique +class TRANSITIONS(enum.Enum): + START = "Start" + SEND_PARTICLE1 = "Send first" # Change + SEND_PARTICLE2 = "Send second" # Change + SEND_MYSTERY = "Send mystery particle" + RESEND = "Resend" + VOTE = "Vote" + RESULTS = "Show results" + CLEAR = "Clear" + def equals(self, string): + return self.value == string + +TRANSITION_GRAPH = { + TRANSITIONS.SEND_PARTICLE1 : [TRANSITIONS.SEND_PARTICLE1, TRANSITIONS.SEND_PARTICLE2, TRANSITIONS.SEND_MYSTERY], + TRANSITIONS.SEND_PARTICLE2 : [TRANSITIONS.SEND_PARTICLE1, TRANSITIONS.SEND_PARTICLE2, TRANSITIONS.SEND_MYSTERY], + TRANSITIONS.SEND_MYSTERY : [TRANSITIONS.RESEND,TRANSITIONS.VOTE], + TRANSITIONS.RESEND : [TRANSITIONS.RESEND,TRANSITIONS.VOTE], + TRANSITIONS.VOTE : [TRANSITIONS.VOTE,TRANSITIONS.RESULTS], + TRANSITIONS.RESULTS : [TRANSITIONS.SEND_MYSTERY, TRANSITIONS.CLEAR] + } + +logger = logging.getLogger(__name__) + + +class AParticleVotingGame(AGame): + + def __init__(self, event_source: Observable, gateway : DeviceGateway, particle1:PARTICLE, particle2:PARTICLE, visualizationIndex:int) -> None: + super().__init__(event_source, gateway) + self._last_sent_particle = None + self.PARTICLE1 = particle1 + self.PARTICLE2 = particle2 + self.VISUALIZATION_INDEX = visualizationIndex + self._votes = {} + + def start(self) -> None: + super().update_available_transitions([TRANSITIONS.SEND_PARTICLE1, TRANSITIONS.SEND_PARTICLE2]) + + def process_event(self, event:MicroSquadEvent) -> None: + logger.debug("Charges received event {} for device {}: {}".format(event.event_type.name, event.device_id, event.payload)) + self.device_gateway.get_node("players-manager").add_player(event.device_id) + playerNode = self.device_gateway.get_node("player-"+event.device_id) + say_dur= playerNode.get_property("say-duration").value + if say_dur is None or say_dur < 300000: + playerNode.get_property("say-duration").value = 300000 + if event.event_type in EVENTS_SENSOR: + if super().last_fired_transition == TRANSITIONS.VOTE.value: + if event.event_type == EventType.VOTE: + # Store the player's vote + self._votes[event.device_id] = int(event.payload["value"]) + playerNode.get_property("say").value = "🎁" + + + def fire_transition(self, transition) -> None: + super().fire_transition(transition) + # Obtain the next transitions in the graph + # If none, the game can be stopped + next_transitions = TRANSITION_GRAPH.get(TRANSITIONS(self._last_fired_transition), None) + + + if(next_transitions is not None and len(next_transitions) > 0): + super().update_available_transitions(next_transitions) + else: + super().update_available_transitions([]) + + last_fired = TRANSITIONS(self._last_fired_transition) + if(last_fired == TRANSITIONS.SEND_PARTICLE1): + # TODO : Add images and sounds on the scoreboard + logger.debug("Sending particle "+self.PARTICLE1.identifier) + + super().device_gateway.update_broadcast("show,p={},v={}".format(self.PARTICLE1.idx, self.VISUALIZATION_INDEX)) + elif(last_fired == TRANSITIONS.SEND_PARTICLE2): + # TODO : Add images and sounds on the scoreboard + logger.debug("Sending particle "+self.PARTICLE2.identifier) + super().device_gateway.update_broadcast("show,p={},v={}".format(self.PARTICLE2.idx, self.VISUALIZATION_INDEX)) + elif(last_fired == TRANSITIONS.SEND_MYSTERY): + self._votes = {} + for pn in self.get_all_player_nodes(): + pn.get_property("animation").value = "Idle" + pn.get_property("say").value = "" + self._last_sent_particle = self.get_random_particle() + logger.debug("Sending {}".format(self._last_sent_particle.identifier)) + super().device_gateway.update_broadcast("show,p={},v={}".format(self._last_sent_particle.idx, self.VISUALIZATION_INDEX)) + elif(last_fired == TRANSITIONS.RESEND): + logger.debug("Re-Sending {}".format(self._last_sent_particle.identifier)) + super().device_gateway.update_broadcast("show,p={},v={}".format(self._last_sent_particle.idx, self.VISUALIZATION_INDEX)) + elif(last_fired == TRANSITIONS.VOTE): + super().device_gateway.update_broadcast("vote,v=2") + elif(last_fired == TRANSITIONS.RESULTS): + # Tally up the votes, make players say the result, change their animation (DEATH if they are wrong) + for player_id,vote_value in self._votes.items(): + player_node = self.get_player_node_by_id(player_id) + if(vote_value == self._last_sent_particle.idx): + # Correct vote ! + player_node.get_property("say").value = "" + player_node.get_property("animation").value = "Idle" + else: + _defeat(player_node, "❌") + for player_node in self.get_all_player_nodes(): + if(player_node.get_property("terminal-id").value not in self._votes.keys()): + _defeat(player_node, "❓") + elif(last_fired == TRANSITIONS.CLEAR): + for player_node in self.get_all_player_nodes(): + player_node.get_property("say").value = "" + player_node.get_property("animation").value = "Idle" + + def get_random_particle(self) -> PARTICLE: + return random.choice([self.PARTICLE1, self.PARTICLE2]) + + def stop(self) -> None: + print("{} stopped".format(__name__)) + + +def _defeat(player_node:NodePlayer,emoji_entity:str): + player_node.get_property("say").value = "{}".format(emoji_entity) + player_node.get_property("animation").value = "Death" \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/game/particles.py b/modules/gateway/src/main/python/microsquad/game/particles.py new file mode 100644 index 0000000..3cc1314 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/game/particles.py @@ -0,0 +1,23 @@ +import enum + +@enum.unique +class PARTICLE(enum.Enum): + ELECTRON = ("electron",0) + PROTON = ("proton",1) + PHOTON = ("photon", 2) + NEUTRON = ("neutron",3) + POSITRON = ("positron",4) + ANTIPROTON = ("antiproton",5) + + def __init__(self, identifier : str, idx : int) -> None: + self.idx = idx + self.identifier = identifier + + +PARTICLES = list(PARTICLE) + +def find_emote_by_idx(idx:int) -> PARTICLE: + return next((p for p in list(PARTICLE) if p.idx == idx), None) + +def find_emote_by_ide(id:str) -> PARTICLE: + return next((p for p in list(PARTICLE) if p.identifier == id), None) diff --git a/modules/gateway/src/main/python/microsquad/game/summary.png b/modules/gateway/src/main/python/microsquad/game/summary.png new file mode 100644 index 0000000..be87cb7 Binary files /dev/null and b/modules/gateway/src/main/python/microsquad/game/summary.png differ diff --git a/modules/gateway/src/main/python/microsquad/gateway.py b/modules/gateway/src/main/python/microsquad/gateway.py deleted file mode 100644 index 6eca74a..0000000 --- a/modules/gateway/src/main/python/microsquad/gateway.py +++ /dev/null @@ -1,39 +0,0 @@ -from microbit import display,radio, sleep - -import paho.mqtt.client as mqtt - -# The callback for when the client receives a CONNACK response from the server. -def on_connect(client, userdata, flags, rc): - print("uSquad Gateway Connected with result code "+str(rc)) - # Subscribing in on_connect() means that if we lose the connection and - # reconnect then subscriptions will be renewed. - client.subscribe("homie/usquad/gateway/#") - -# The callback for when a PUBLISH message is received from the server. -def on_message(client, userdata, msg): - print(msg.topic+" "+str(msg.payload.decode('ascii'))) - radio.send(str(msg.payload.decode('ascii'))) - - - -radio.config(length=200, channel=12, group=1) -radio.on() - - -client = mqtt.Client() -client.on_connect = on_connect -client.on_message = on_message - -client.connect("broker.hivemq.com", 1883, 60) - -# Blocking call that processes network traffic, dispatches callbacks and -# handles reconnecting. -# Other loop*() functions are available that give a threaded interface and a -# manual interface. -client.loop_start() - -while True: - msg = radio.receive() - if msg != "None": - print(msg) - sleep(100) \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/gateway/dummy/dummy_gateway.py b/modules/gateway/src/main/python/microsquad/gateway/dummy/dummy_gateway.py new file mode 100644 index 0000000..d871e80 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/gateway/dummy/dummy_gateway.py @@ -0,0 +1,60 @@ + +import logging +import time + +from rx3 import Observable +from rx3.subject import Subject + +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway +from microsquad.mapper.homie.homie_mapper import HomieMapper +from microsquad.connector.dummy_connector import DummyConnector + +gateway = None + +class HomieDummyGateway: + """ + MicroSquad Gateway Dummy Homie implementation. + The Dummy implementation does not actually connect to Microbit terminals. It is used primarily for interactive testing. + """ + def __init__(self, homie_settings, mqtt_settings, event_source: Observable): + self._event_source = event_source + self._homie_settings = homie_settings + self._mqtt_settings = mqtt_settings + self.deviceGateway = DeviceGateway(event_source = self._event_source, homie_settings=self._homie_settings,mqtt_settings=self._mqtt_settings) + self.mapper = HomieMapper(self.deviceGateway, self._event_source) + self.connector = DummyConnector(self.mapper, self._event_source) + + def start(self): + self.deviceGateway.start() + self.connector.start() + + @property + def event_source(self): + return self._event_source + +def main(): + global gateway + MQTT_SETTINGS = { + 'MQTT_BROKER' : 'localhost', + 'MQTT_PORT' : 1883, + 'MQTT_SHARE_CLIENT': True + } + + HOMIE_SETTINGS = { + "update_interval": 1, + "topic": "microsquad" + } + + gateway = HomieDummyGateway(HOMIE_SETTINGS, MQTT_SETTINGS, Subject()) + print("Starting dummy gateway...") + gateway.start() + # dev_id = "1234-5435" + # gateway.connector.simulate_message("bonjour,dev_id={}".format(dev_id)) + # gateway.connector.simulate_message("read_button,button=\"a\",dev_id={} 123456978".format(dev_id)) + + # while True: + # time.sleep(5) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/gateway/mqtt/__main__.py b/modules/gateway/src/main/python/microsquad/gateway/mqtt/__main__.py new file mode 100644 index 0000000..52b359e --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/gateway/mqtt/__main__.py @@ -0,0 +1,43 @@ +from dotenv import load_dotenv +import time +import argparse +import logging +from homie.device_base import HOMIE_SETTINGS + + +from .homie_bitio_gateway import HomieBitioGateway + +import rx3 + +load_dotenv() + +logging.basicConfig(encoding='ascii', level=logging.INFO) +logging.getLogger('homie').setLevel(logging.WARN) + +MQTT_SETTINGS = { + 'MQTT_BROKER' : 'localhost', + 'MQTT_PORT' : 1883, + 'MQTT_SHARE_CLIENT': True + } + +HOMIE_SETTINGS = { + "update_interval": 1, + "topic": "microsquad" + } + +# parser = argparse.ArgumentParser(description='Run a MicroSquad gateway.') +# parser.add_argument('-t','--test', action='store_true', +# help='Run the gateway in interactive mode without a Microbit connector') +# parser.add_argument('--connector', type=ascii, default="bitio", choices=["dummy","bitio"], +# help='Specify the connector you are using') + +# args = parser.parse_args() + + +event_source = rx3.subject.Subject() + +gateway = HomieBitioGateway(HOMIE_SETTINGS, MQTT_SETTINGS, event_source) +gateway.start() + +while True: + time.sleep(5) \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/gateway/mqtt/homie_bitio_gateway.py b/modules/gateway/src/main/python/microsquad/gateway/mqtt/homie_bitio_gateway.py new file mode 100644 index 0000000..97aa68c --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/gateway/mqtt/homie_bitio_gateway.py @@ -0,0 +1,37 @@ +import paho.mqtt.client as mqtt + +import logging + +from rx3.subject import Subject + +from ...game.game_manager import GameManager + +from ...mapper.homie.gateway.device_gateway import DeviceGateway +from ...mapper.homie.homie_mapper import HomieMapper +from ...connector.bitio_connector import BitioConnector + +class HomieBitioGateway: + + + """ + MicroSquad Gateway MQTT Homie implementation. + Using provided MQTT connection parameters, this gateway declares a series of Homie devices on the MQTT broker + that can be used to interact with MicroSquad entities (players, terminals, teams, scoreboard etc...). + Remote method calls are implemented as Homie settable properties. + Events are propagated using RxPy. + """ + def __init__(self, homie_settings, mqtt_settings, event_source): + + self._event_source = event_source + self._homie_settings = homie_settings + self._mqtt_settings = mqtt_settings + self._gatewayDevice = DeviceGateway(event_source = self._event_source, homie_settings=self._homie_settings,mqtt_settings=self._mqtt_settings) + self._mapper = HomieMapper(self._gatewayDevice, self._event_source) + self._connector = BitioConnector(self._mapper, self._event_source) + self._game_manager = GameManager(self._event_source, self._gatewayDevice) + + def start(self): + self._gatewayDevice.start() + self._connector.start() + + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/mapper/abstract_mapper.py b/modules/gateway/src/main/python/microsquad/mapper/abstract_mapper.py new file mode 100644 index 0000000..330316c --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/abstract_mapper.py @@ -0,0 +1,29 @@ +from abc import ABCMeta,abstractmethod + +from rx3 import Observable + +class AbstractMapper(metaclass=ABCMeta): + """ + Maps communication events between terminals and MQTT. + Events are propagated using an RxPy observable. + """ + def __init__(self, event_source: Observable) -> None: + self._event_source = event_source + + @property + def event_source(self) -> Observable: + return self._event_source + + @abstractmethod + def map_from_mqtt(self, message): + pass + + @abstractmethod + def map_from_microbit(self, message): + pass + + + + + + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/device_gateway.py b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/device_gateway.py new file mode 100644 index 0000000..32c7e95 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/device_gateway.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python + +import logging + +from homie.device_base import Device_Base + +from homie.node.property.property_string import Property_String +from homie.node.property.property_boolean import Property_Boolean +from homie.node.node_base import Node_Base +from rx3 import Observable + +from ..terminal.device_terminal import DeviceTerminal +from ....event import MicroSquadEvent,EventType + +from .node_player_manager import NodePlayerManager + +from .node_team_manager import NodeTeamManager + +logger = logging.getLogger(__name__) + + +class DeviceGateway(Device_Base): + """ + The Gateway device exposes properties of the microsquad gateway. + + It can be used to read properties of the ongoing game, players and teams currently active. + """ + + def __init__( + self, + event_source : Observable, + device_id= "gateway", + name="MicroSquad Gateway", + homie_settings=None, + mqtt_settings=None + ): + super().__init__(device_id, name, homie_settings, mqtt_settings) + + # Keep track of mqtt settings to instantiate Terminal devices + self._mqtt_settings = mqtt_settings + + self._scoreboard = Node_Base(self,id="scoreboard", name="Scoreboard", type_="scoreboard") + self.add_node(self._scoreboard) + self._scoreboard.add_property(Property_String(node = self._scoreboard, id="score",name="Score" )) + self._scoreboard.add_property(Property_String(node = self._scoreboard, id="image",name="Image" )) + self._scoreboard.add_property(Property_String(node = self._scoreboard, id="sound", name="Sound", retained=False )) + self._scoreboard.add_property(Property_String(node = self._scoreboard, id="show",name="Show" )) + + self._player_manager = NodePlayerManager(self) + self.add_node(self._player_manager) + + self._team_manager = NodeTeamManager(self) + self.add_node(self._team_manager) + + self._terminals = {} + + self._game_node = Node_Base(self,id="game", name="game", type_="game") + self.add_node(self._game_node) + + self._game_node.add_property(Property_String(node = self._game_node, settable= True, set_value =self.update_game, id="name",name="Name" )) + self._last_known_game : str = None + + self._game_node.add_property(Property_String(node = self._game_node, id="game-status",name="Game Status" )) + self._game_node.add_property(Property_String(node = self._game_node, id="transitions",name="Transitions" )) + self._game_node.add_property(Property_String(node = self._game_node, settable= True, set_value =self.fire_transition, id="fire-transition",name="The transition fired to further the game's progression" )) + # self._game.add_property(Property_String(node = self._game, id="audience-code",name="audience-code" )) + # self._game.add_property(Property_String(node = self._game, id="admin-code",name="admin-code" )) + self._game_node.add_property(Property_String(node = self._game_node, settable= True, set_value =self.update_broadcast, id="broadcast",name="Broadcast" )) + + self._event_source = event_source + if self._event_source is None: + raise ValueError("Gateway must be passed an event source.") + + + def add_terminal(self, device_id : str): + if(device_id not in self.terminals.keys()): + terminal = DeviceTerminal(event_source = self._event_source,device_id = "terminal-"+str(device_id), name="Terminal "+str(device_id), homie_settings=self.homie_settings, mqtt_settings=self._mqtt_settings) + terminal.get_node("info").get_property("terminal-id").value = device_id + terminal.get_node("info").get_property("serial-number").value = device_id + logging.info("Added new terminal {}".format(device_id)) + self._terminals[device_id] = terminal + terminal.start() + self._event_source.on_next(MicroSquadEvent(EventType.TERMINAL_DISCOVERED, device_id)) + + @property + def terminals(self): + return self._terminals + + @property + def game_node(self): + return self._game_node + + def update_game(self, new_game): + """ + A new game should be started, execute it. + """ + if(new_game is None or new_game == ""): + self._event_source.on_next(MicroSquadEvent(EventType.GAME_STOP)) + elif(not(self._game_node.get_property("game-status").value == EventType.GAME_START) or (self._last_known_game != new_game)): + self._event_source.on_next(MicroSquadEvent(EventType.GAME_START, payload=new_game)) + self._last_known_game = new_game + + def update_broadcast(self, command): + """ + A new broadcast command has been sent, we need to propagate it to all terminals + """ + self._event_source.on_next(MicroSquadEvent(EventType.TERMINAL_BROADCAST,payload = command)) + + def fire_transition(self, transition): + """ + A new transition has been fired, we need to propagate it to the game + """ + self._event_source.on_next(MicroSquadEvent(EventType.GAME_TRANSITION,payload = transition)) \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_player.py b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_player.py new file mode 100644 index 0000000..6c60a43 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_player.py @@ -0,0 +1,47 @@ +import logging + +from homie.node.property.property_string import Property_String +from homie.node.property.property_datetime import Property_DateTime +from homie.node.property.property_integer import Property_Integer +from homie.node.property.property_float import Property_Float + +from homie.node.node_base import Node_Base + +logger = logging.getLogger(__name__) + +class NodePlayer(Node_Base): + _instance_count = 1 + + def __init__( + self, + device, + id="player", + name="Player", + type_="player", + retain=True, + order=0, + qos=1 + ): + super().__init__(device, id, name, type_, retain, qos) + + self.add_property(Property_String(self, id="nickname", name="Nickname", settable=True, set_value=self.update_nickname)) + self.add_property(Property_String(self, id="skin", name="skin")) + self.add_property(Property_String(self, id="order", name="Order", value = order, settable=True, set_value = self.update_order)) + self.add_property(Property_Float(self, id="scale", name="Scale", value = 1.0, settable=False)) + self.add_property(Property_Float(self, id="rotation", name="Rotation", value = 1.0, settable=False)) + self.add_property(Property_String(self, id="say", name="Say")) + self.add_property(Property_DateTime(self, id="say-start", name="say start")) + self.add_property(Property_Integer(self, id="say-duration", name="say duration", settable=False)) + self.add_property(Property_String(self, id="animation", name="animation", retained=False)) + self.add_property(Property_DateTime(self, id="animation-start", name="animation start")) + self.add_property(Property_Integer(self, id="animation-duration", name="animation duration", settable=False)) + self.add_property(Property_String(self, id="accessory", name="accessory")) + self.add_property(Property_String(self, id="terminal-id", name="terminal id")) + + + def update_nickname(self,value:str): + self.get_property("nickname").value = value + + def update_order(self,value:str): + self.get_property("order").value = value + diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_player_manager.py b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_player_manager.py new file mode 100644 index 0000000..5138d40 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_player_manager.py @@ -0,0 +1,44 @@ +import logging + +from homie.node.property.property_string import Property_String +from homie.node.node_base import Node_Base + +from .node_player import NodePlayer + +logger = logging.getLogger(__name__) + +class NodePlayerManager(Node_Base): + + def __init__(self, device): + super().__init__(device, id="players-manager", name="Players Manager", type_="players_manager", retain=True, qos=1) + + self.add_property(Property_String(self, id="add", settable=True, name="add player", set_value = self.add_player )) + self.add_property(Property_String(self, id="remove", settable=True, name="remove player", set_value = self.remove_player )) + + self.players = [] + self.player_counter = 0 + self.add_property(Property_String(self, id="list", name="list" )) + + def remove_player(self,identifier): + if(identifier in self.players): + logger.info("Removing Player : {}".format(identifier)) + self.players.remove(identifier) + self.device.remove_node("player-"+identifier) + self.get_property("list").value = ",".join(self.players) + + def add_player(self,identifier): + """ + TODO : Split the identifier either: + - id + - id:name + - id:name:nickname + - or empty (random UUID) + """ + if(identifier not in self.players): + self.player_counter += 1 + new_player = NodePlayer(self.device,id="player-"+identifier, name=identifier, order = identifier) + new_player.get_property("terminal-id").value = identifier + self.device.add_node(new_player) + self.players.append(identifier) + self.get_property("list").value = ",".join(self.players) + logger.info("Player Added : {}".format(identifier)) diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_team.py b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_team.py new file mode 100644 index 0000000..0f326a5 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_team.py @@ -0,0 +1,32 @@ +import logging + +from homie.node.property.property_string import Property_String +from homie.node.property.property_datetime import Property_DateTime +from homie.node.property.property_integer import Property_Integer +from homie.node.property.property_color import Property_Color +from homie.node.node_base import Node_Base + +logger = logging.getLogger(__name__) + +class NodeTeam(Node_Base): + _instance_count = 1 + + def __init__( + self, + device, + id="team", + name="Team", + type_="team", + retain=True, + qos=1 + ): + super().__init__(device, id, name, type_, retain, qos) + + self.add_property(Property_String(self, id="nickname" , name="Nickname")) + self.add_property(Property_String(self, id="players" , name="Players")) + self.add_property(Property_String(self, id="terminals", name="Terminals")) + self.add_property(Property_String(self, id="say", name="Say")) + self.add_property(Property_String(self, id="animation", name="Animation")) + # hexadecimal color value (replace with Color Property when implemented in Homie lib) + self.add_property(Property_String(self, id="color", name="Color")) + diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_team_manager.py b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_team_manager.py new file mode 100644 index 0000000..33ccbb4 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/gateway/node_team_manager.py @@ -0,0 +1,85 @@ +import logging + +from homie.node.property.property_string import Property_String +from homie.node.node_base import Node_Base + +from .node_team import NodeTeam + +import json + +logger = logging.getLogger(__name__) + +class NodeTeamManager(Node_Base): + + def __init__(self, device): + super().__init__(device, id="teams-manager", name="Teams Manager", type_="teams_manager", retain=True, qos=1) + + self.add_property(Property_String(self, id="add-player", settable=True, name="add player", set_value = self.add_player )) + self.add_property(Property_String(self, id="remove-player", settable=True, name="remove player", set_value = self.remove_player )) + self.add_property(Property_String(self, id="add", settable=True, name="add team", set_value = self.add_team )) + self.add_property(Property_String(self, id="remove", settable=True, name="remove team", set_value = self.remove_team )) + + self.teams = [] + self.add_property(Property_String(self, id="list", name="list teams" )) + self.teams_to_players = {} + self.add_property(Property_String(self, id="list-players", name="list_players" )) + + def refresh_teams_list(self): + self.get_property("list").value = json.dumps(self.teams, sort_keys=True,separators=(',', ':')) + self.get_property("list-players").value = json.dumps(self.teams_to_players, sort_keys=True,separators=(',', ':')) + + def add_team(self,team): + if(not team in self.teams): + self.device.add_node(NodeTeam(self.device,id="team-"+team, name=team)) + self.teams.append(team) + if(team not in self.teams_to_players.keys()): + self.teams_to_players[team] = [] + self.refresh_teams_list() + logger.info("Team Added : {}".format(team)) + else: + logger.error("Team {} already exists. Not added.".format(team)) + + def remove_team(self,team): + if(team in self.teams): + logger.info("Removing team : {}".format(team)) + self.teams.remove(team) + self.teams_to_players.pop(team) + self.device.remove_node("team-"+team) + self.refresh_teams_list() + else: + logger.info("Team {} does not exist. Not removed.".format(team)) + + def add_player(self,identifier_team_player): + team,player = identifier_team_player.split(":",1) + if(team in self.teams): + logger.info("Adding Player {} to Team {}".format(player,team)) + if(player not in self.teams_to_players[team]): + self.teams_to_players[team].append(player) + self.refresh_team_node(team) + self.refresh_teams_list() + logger.debug("Added Player {} to Team {}".format(player,team)) + else: + logger.debug("Player {} is already in Team {} !".format(player,team)) + else: + logger.info("Team {} does not exist. Not adding player {}.".format(team, player)) + + def remove_player(self,identifier_team_player): + team,player = identifier_team_player.split(":",1) + if(team in self.teams): + logger.info("Removing Player {} from Team {}".format(player,team)) + if(team in self.teams_to_players.keys()): + if(player in self.teams_to_players[team]): + self.teams_to_players[team].remove(player) + self.refresh_teams_list() + logger.debug("Removed Player {} from Team {}".format(player,team)) + else: + logger.info("Team {} does not exist. Not removing player {}.".format(team, player)) + + def refresh_team_node(self,team_to_refresh): + players_list = self.teams_to_players[team_to_refresh] + self.device.get_node("team-"+team_to_refresh).get_property("players").value = ",".join(players_list) + + + + + diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/homie_mapper.py b/modules/gateway/src/main/python/microsquad/mapper/homie/homie_mapper.py new file mode 100644 index 0000000..3fe2480 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/homie_mapper.py @@ -0,0 +1,97 @@ +from ..line_protocol_parser import LineProtocolParser +from rx3 import Observable + +from .gateway.device_gateway import DeviceGateway + +import datetime +import logging + +from ..abstract_mapper import AbstractMapper + +from ...event import EventType,MicroSquadEvent + +logger = logging.getLogger(__name__) + +def _add_properties_to_tags(node, properties, tags) -> None: + for prop in properties: + tags[prop] = node.get_property(prop).value + +class HomieMapper(AbstractMapper): + """ + Homie V4 Mapper - converts incoming MQTT and Microbit radio messages to Homie V4 devices, nodes and properties. + """ + def __init__(self, gateway: DeviceGateway, event_source: Observable) -> None: + super().__init__(event_source) + self._gateway = gateway + self._parser = LineProtocolParser() + + + def map_from_mqtt(self, message): + """ With a Homie implementation, we are not mapping low-level MQTT messages + but rather update calls made on properties. + This is therefore a no-op implementation. + Instead, we implement a RxPy observable, and pass on all command events + to the connector. + """ + pass + + + + def map_from_microbit(self, message): + # TODO: The mapper could become generic and only parse line protocol events + # to transform them into reactive events. + try: + # logger.debug(">> Raw message '" + message+"'") + msg = self._parser.parse(message) + measurement = msg[0] + tags = msg[1] + dev_id = tags["dev_id"] + + # This is No-op if the terminal is already known to the gateway + self._gateway.add_terminal(dev_id) + + # Interpret measurement, Convert fields and tags to Homie device update + if measurement == EventType.BONJOUR.value: + self.event_source.on_next(MicroSquadEvent(EventType.BONJOUR,dev_id,tags.copy())) + elif measurement.startswith("read_"): + # e.g. "read_button" + read,verb = measurement.split("_",1) + + terminal = self._gateway.terminals[dev_id] + if verb == EventType.BUTTON.value: + # Button A or B ? + button_id = "button-"+tags["button"] + button_node = terminal.get_node(button_id) + if(button_node is not None): + button_node.get_property("pressed").value=1 + button_node.get_property("last").value=datetime.datetime.now().isoformat() + button_node.get_property("count").value +=1 + _add_properties_to_tags(button_node,["pressed","last", "count"],tags) + self.event_source.on_next(MicroSquadEvent(EventType.BUTTON,dev_id,tags.copy())) + else: + logging.warn("Button {} is not defined as device node !".format("button_id")) + + # TODO : Set a timer to reset the pressed state later + # Could be easily done with RxPy + elif verb == EventType.ACCELERATOR.value: + accel_node = terminal.get_node("accel") + accel_node.get_property("x").value=int(tags["x"]) + accel_node.get_property("y").value=int(tags["y"]) + accel_node.get_property("z").value=int(tags["z"]) + accel_node.get_property("value").value="{x},{y},{z}".format(**tags) + _add_properties_to_tags(accel_node,["value"],tags) + self.event_source.on_next(MicroSquadEvent(EventType.ACCELERATOR,dev_id,tags.copy())) + elif verb == EventType.VOTE.value: + vote_node = terminal.get_node("vote") + vote_node.get_property("value").value=(tags["value"]) + vote_node.get_property("index").value=int(tags["index"]) + vote_node.get_property("last").value=datetime.datetime.now().isoformat() + _add_properties_to_tags(vote_node,["last"],tags) + self.event_source.on_next(MicroSquadEvent(EventType.VOTE,dev_id,tags.copy())) + elif verb == EventType.TEMPERATURE.value: + terminal.get_node("temperature").get_property("temperature").value=int(tags["value"]) + self.event_source.on_next(MicroSquadEvent(EventType.TEMPERATURE,dev_id,tags.copy())) + except: + logging.exception("Unexpected error on line message : %s",message) + raise + diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/device_terminal.py b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/device_terminal.py new file mode 100644 index 0000000..94fd509 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/device_terminal.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +import logging + +from homie.device_base import Device_Base +from homie.node.property.property_string import Property_String +from homie.node.property.property_temperature import Property_Temperature +from rx3 import Observable + + +from ....event import EventType,MicroSquadEvent + +from .node_accelerator import NodeAccelerator +from .node_button import NodeButton +from .node_display import NodeDisplay +from .node_temperature import NodeTemperature +from .node_vote import NodeVote +from .node_info import NodeInfo + +logger = logging.getLogger(__name__) + + +class DeviceTerminal(Device_Base): + def __init__( self, event_source : Observable, device_id=None, name=None, homie_settings=None, mqtt_settings=None): + super().__init__(device_id, name, homie_settings, mqtt_settings) + + self.add_node(NodeAccelerator(self)) + self.add_node(NodeButton(self,id="button-a",name="Button A")) + self.add_node(NodeButton(self,id="button-b",name="Button B")) + self.add_node(NodeDisplay(self)) + self.add_node(NodeTemperature(self)) + self.add_node(NodeVote(self)) + self.add_node(NodeInfo(self, command_handler= self.update_command)) + + self._event_source = event_source + if self._event_source is None: + raise ValueError("Terminal must be passed an event source.") + + def update_command(self, command): + self._event_source.on_next(MicroSquadEvent(EventType.TERMINAL_COMMAND, device_id= self.get_node("info").get_property("terminal-id").value,payload=command)) + + + diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_accelerator.py b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_accelerator.py new file mode 100644 index 0000000..7551329 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_accelerator.py @@ -0,0 +1,26 @@ +import logging + +from homie.node.property.property_integer import Property_Integer +from homie.node.property.property_string import Property_String +from homie.node.node_base import Node_Base + +logger = logging.getLogger(__name__) + +class NodeAccelerator(Node_Base): + def __init__( + self, + device, + id = "accel", + name = "Accelerator", + type_="accelerator", + retain=True, + qos=1 + ): + super().__init__(device, id, name, type_, retain, qos) + + self.add_property(Property_Integer(self, id="x", name="x", settable=False)) + self.add_property(Property_Integer(self, id="y", name="y", settable=False)) + self.add_property(Property_Integer(self, id="z", name="z", settable=False)) + self.add_property(Property_String(self, id="value", name="Value", settable=False)) + + diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_button.py b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_button.py new file mode 100644 index 0000000..1731cea --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_button.py @@ -0,0 +1,26 @@ +import logging + +from homie.node.property.property_string import Property_String +from homie.node.property.property_datetime import Property_DateTime +from homie.node.property.property_integer import Property_Integer +from homie.node.node_base import Node_Base + +logger = logging.getLogger(__name__) + +class NodeButton(Node_Base): + def __init__( + self, + device, + id, + name, + type_="button", + retain=True, + qos=1 + ): + super().__init__(device, id, name, type_, retain, qos) + + self.add_property(Property_Integer(self, id="pressed", value=0, name="Pressed", settable=False)) + self.add_property(Property_Integer(self, id="count", value=0,name="Pressed count", settable=False)) + self.add_property(Property_DateTime(self, id="last", name="Last pressed timestamp")) + + diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_display.py b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_display.py new file mode 100644 index 0000000..41cbcdc --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_display.py @@ -0,0 +1,24 @@ +import logging + +from homie.node.property.property_string import Property_String +from homie.node.property.property_dimmer import Property_Dimmer +from homie.node.node_base import Node_Base + +logger = logging.getLogger(__name__) + +class NodeDisplay(Node_Base): + def __init__( + self, + device, + id = "display", + name = "Display", + type_="display", + retain=True, + qos=1 + ): + super().__init__(device, id, name, type_, retain, qos) + + self.add_property(Property_String(self, id="contents", name="contents", settable=False)) + self.add_property(Property_Dimmer(self, id="luminosity", name="luminosity", settable=False)) + + diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_info.py b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_info.py new file mode 100644 index 0000000..d164a6d --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_info.py @@ -0,0 +1,26 @@ +import logging + +from homie.node.property.property_string import Property_String +from homie.node.property.property_datetime import Property_DateTime +from homie.node.node_base import Node_Base + +logger = logging.getLogger(__name__) + +class NodeInfo(Node_Base): + def __init__( + self, + device, + command_handler, + id = "info", + name = "Info", + type_="info", + retain=True, + qos=1 + ): + super().__init__(device, id, name, type_, retain, qos) + + self.add_property(Property_String(self, id="terminal-id", name="Terminal ID")) + self.add_property(Property_String(self, id="serial-number", name="Serial Number")) + self.add_property(Property_DateTime(self, id="heartbeat", name="Heartbeat")) + self.add_property(Property_String(self, id="command", name="Command", settable=True, set_value=command_handler, value="", retained = False)) + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_temperature.py b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_temperature.py new file mode 100644 index 0000000..a99bcac --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_temperature.py @@ -0,0 +1,23 @@ +import logging + +from homie.node.property.property_temperature import Property_Temperature +from homie.node.node_base import Node_Base + +logger = logging.getLogger(__name__) + +class NodeTemperature(Node_Base): + def __init__( + self, + device, + id = "temperature", + name = "Temperature", + temp_units="C", + type_="temperature", + retain=True, + qos=1 + ): + super().__init__(device, id, name, type_, retain, qos) + self.temp_units = temp_units + self.temperature = Property_Temperature(self, unit=self.temp_units) + self.add_property(self.temperature) + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_vote.py b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_vote.py new file mode 100644 index 0000000..2376a62 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/homie/terminal/node_vote.py @@ -0,0 +1,25 @@ +import logging + +from homie.node.property.property_string import Property_String +from homie.node.property.property_datetime import Property_DateTime +from homie.node.property.property_integer import Property_Integer +from homie.node.node_base import Node_Base + +logger = logging.getLogger(__name__) + +class NodeVote(Node_Base): + def __init__( + self, + device, + id = "vote", + name = "Vote", + type_="vote", + retain=True, + qos=1 + ): + super().__init__(device, id, name, type_, retain, qos) + + self.add_property(Property_String(self, id="value", name="Choice value")) + self.add_property(Property_Integer(self, id="index", name="Choice index", settable=False)) + self.add_property(Property_DateTime(self, id="last", name="Last vote timestamp")) + \ No newline at end of file diff --git a/modules/gateway/src/main/python/microsquad/mapper/line_protocol_parser.py b/modules/gateway/src/main/python/microsquad/mapper/line_protocol_parser.py new file mode 100644 index 0000000..f87c8e3 --- /dev/null +++ b/modules/gateway/src/main/python/microsquad/mapper/line_protocol_parser.py @@ -0,0 +1,60 @@ +import time + +def _pop_head_or_none(arr, peek_only = False): + """ + Simple static utility function that can pop or peek the head of an array list + and return None if it is empty + """ + if arr and len(arr)>0: + if peek_only: + return arr[0] + else: + return arr.pop(0) + else: + return None + +class LineProtocolParser: + """ + A simple, homemade, self-contained Line protocol parser. + The parser tolerates even non-standard line protocol messages (e.g. missing fields). + It does not strictly implement the line protocol standard. Use at your own risks. + """ + def parse(self,msg): + measure = None + tags = {} + fields = {} + timestamp = None + + frags = msg.split(" ") + frag = _pop_head_or_none(frags) + if frag is not None: + measFrags = frag.split(",") + if len(measFrags) > 0: + measure = measFrags[0] + if len(measFrags) > 1: + for tagFrag in measFrags[1:]: + tagKV = tagFrag.split("=") + tags[tagKV[0]] = tagKV[1].strip('"\'') + frag = _pop_head_or_none(frags,True) + if frag is not None: + if("=" in frag): + frag = _pop_head_or_none(frags) + fieldsFragment = frag.split(",") + for fieldFragment in map(lambda v: v.split("="),fieldsFragment): + fields[fieldFragment[0]] = float(fieldFragment[1]) + frag = _pop_head_or_none(frags) + if frag is not None: + timestamp = int(frag) + return (measure, tags, fields, timestamp) + + def serialize(self,measurement, tags=None, fields=None, timestamp=None): + result : str = measurement + if tags is not None: + result += (','.join('{}="{}"'.format(key, value) for key, value in tags.items())) + " " + if fields is not None: + result += (','.join('{}={}'.format(key, value) for key, value in fields.items())) + " " + if timestamp is not None: + result += timestamp + else: + result += str(time.time_ns()) + return result \ No newline at end of file diff --git a/modules/gateway/src/main/python/requirements.txt b/modules/gateway/src/main/python/requirements.txt new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/modules/gateway/src/main/python/requirements.txt @@ -0,0 +1 @@ +. diff --git a/modules/gateway/src/main/python/setup.py b/modules/gateway/src/main/python/setup.py index c222d37..7ff5f89 100644 --- a/modules/gateway/src/main/python/setup.py +++ b/modules/gateway/src/main/python/setup.py @@ -1,10 +1,12 @@ from setuptools import setup, find_packages setup( - install_requires=[ 'dotenv','influx_line_protocol>=0.1.4','cs20-microbitio==0.2', 'paho-mqtt==1.5.1'], + setup_requires=['pytest-runner'], + install_requires=[ 'wheel','cs20-microbitio==0.2', 'paho-mqtt==1.5.1', 'RxPy3', 'Homie4', 'python-dotenv', "homieclient"], extras_require={ - 'test':['testfixtures','hbmqtt'] + 'test':['pytest','pytest-cov','hbmqtt'] }, + tests_require=['pytest'], name = 'microsquad-gateway', python_requires= '>=3.4.0', version="0.1", # version = '${VERSION}', @@ -27,9 +29,9 @@ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Education", "Topic :: Software Development", ], diff --git a/modules/gateway/src/test/python/microsquad/connector/test_dummy_homie_connector.py b/modules/gateway/src/test/python/microsquad/connector/test_dummy_homie_connector.py new file mode 100644 index 0000000..57058f5 --- /dev/null +++ b/modules/gateway/src/test/python/microsquad/connector/test_dummy_homie_connector.py @@ -0,0 +1,56 @@ +import unittest + +import time +from microsquad.event import EventType, MicroSquadEvent + +import microsquad.gateway.dummy.dummy_gateway as dummy + +import logging +logging.getLogger('homie').setLevel(logging.WARN) + +DEVICE_ID = '12546-4656' + + +class TestDummyHomieConnector(unittest.TestCase): + """ + Test that simulated incoming microbit messages are properly parsed into Device / Node / Properties + """ + + def setUp(self) -> None: + dummy.main() + dummy.gateway.connector.simulate_message("bonjour,dev_id={}".format(DEVICE_ID)) + # Wait for the message to be processed + time.sleep(0.1) + return super().setUp() + + def test_bonjour_message(self): + assert dummy.gateway.deviceGateway.terminals[DEVICE_ID] is not None + + def test_button_read(self): + assert dummy.gateway.deviceGateway.terminals[DEVICE_ID].get_node("button-a").get_property("pressed").value == 0 + dummy.gateway.connector.simulate_message("read_button,button=\"a\",dev_id=\"{}\" 123456978".format(DEVICE_ID)) + time.sleep(1) + assert dummy.gateway.deviceGateway.terminals[DEVICE_ID].get_node("button-a").get_property("pressed").value == 1 + + def test_accel_read(self): + assert dummy.gateway.deviceGateway.terminals[DEVICE_ID].get_node("accel").get_property("x").value is None + + dummy.gateway.connector.simulate_message("read_accel,x=500,y=300,z=-823,dev_id={} 123456978".format(DEVICE_ID)) + time.sleep(1) + assert dummy.gateway.deviceGateway.terminals[DEVICE_ID].get_node("accel").get_property("x").value == 500 + + def test_vote_read(self): + assert dummy.gateway.deviceGateway.terminals[DEVICE_ID].get_node("vote").get_property("value").value is None + dummy.gateway.connector.simulate_message("read_vote,value=\"elephant\",index=3,dev_id={} 123456978".format(DEVICE_ID)) + time.sleep(2) + assert dummy.gateway.deviceGateway.terminals[DEVICE_ID].get_node("vote").get_property("value").value == 'elephant' + + def test_handle_rxpy_broadcast(self): + # Test that the AbstractConnector handles TERMINAL_BROADCAST events as expected + dummy.gateway.event_source.on_next(MicroSquadEvent(EventType.TERMINAL_BROADCAST,payload="buttons")) + # ... it should emerge out of the connector + assert dummy.gateway.connector.last_sent=="buttons" + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/modules/gateway/src/test/python/microsquad/game/customeeze/test_customeeze.py b/modules/gateway/src/test/python/microsquad/game/customeeze/test_customeeze.py new file mode 100644 index 0000000..dd6dc3c --- /dev/null +++ b/modules/gateway/src/test/python/microsquad/game/customeeze/test_customeeze.py @@ -0,0 +1,6 @@ +import unittest + + + +if __name__ == '__main__': + unittest.main() diff --git a/modules/gateway/src/test/python/microsquad/game/my_test_game/my_test_game.py b/modules/gateway/src/test/python/microsquad/game/my_test_game/my_test_game.py new file mode 100644 index 0000000..c740e18 --- /dev/null +++ b/modules/gateway/src/test/python/microsquad/game/my_test_game/my_test_game.py @@ -0,0 +1,35 @@ +from rx3 import Observable +from microsquad.game.abstract_game import AGame +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway +from microsquad.event import MicroSquadEvent + +class Game(AGame): + """ + A simple game that allows to declare new players and customize their appearance + """ + def __init__(self, event_source: Observable, gateway : DeviceGateway) -> None: + super().__init__(event_source, gateway) + self.started = False + self.running = False + self.stopped = False + self.received_events : MicroSquadEvent = [] + + def start(self) -> None: + print("Test Game starting") + self.started = True + self.running = True + self.available_transitions = ["stop","get_events"] + + def process_event(self, event:MicroSquadEvent) -> None: + """ + Handle the next game event + """ + if(self.running and self.last_fired_transition == "get_events"): + self.received_events.append(event) + + def stop(self) -> None: + print("Test Game stopping") + self.started = True + self.running = False + self.stopped = True + diff --git a/modules/gateway/src/test/python/microsquad/game/test_game_manager.py b/modules/gateway/src/test/python/microsquad/game/test_game_manager.py new file mode 100644 index 0000000..3913c87 --- /dev/null +++ b/modules/gateway/src/test/python/microsquad/game/test_game_manager.py @@ -0,0 +1,70 @@ +import unittest + +from rx3.subject import Subject + +import logging +import time + +from microsquad.game.game_manager import GameManager +from microsquad.event import MicroSquadEvent +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway +from microsquad.event import EventType +from microsquad.game.my_test_game.my_test_game import Game as TestGame + +logging.getLogger('homie').setLevel(logging.WARN) + +class TestGameManager(unittest.TestCase): + def setUp(self) -> None: + _mqtt_settings = { + 'MQTT_BROKER' : 'localhost', + 'MQTT_PORT' : 1883, + } + self.received_events: MicroSquadEvent = [] + self.event_source = Subject() + self.gateway = DeviceGateway(event_source = self.event_source,mqtt_settings=_mqtt_settings) + self.game_manager = GameManager(self.event_source, self.gateway) + return super().setUp() + + def test_game_start_stop_via_rxpy(self): + self.event_source.on_next(MicroSquadEvent(EventType.GAME_START, payload="my_test_game")) + time.sleep(0.5) + test_game = self.game_manager.current_game + assert isinstance(test_game,TestGame) + self._verify_events(test_game) + self.event_source.on_next(MicroSquadEvent(EventType.GAME_STOP)) + time.sleep(0.3) + assert self.game_manager.current_game is None + + def test_game_start_stop_via_device_gateway(self): + self.gateway.update_game("my_test_game") + time.sleep(0.5) + test_game = self.game_manager.current_game + assert isinstance(test_game,TestGame) + self._verify_events(test_game) + self.gateway.update_game("") + time.sleep(0.3) + assert self.game_manager.current_game is None + + def _verify_events(self, test_game): + self.event_source.on_next(MicroSquadEvent(EventType.BUTTON, payload={"pressed":1,"count":1})) + self.event_source.on_next(MicroSquadEvent(EventType.BUTTON, payload={"pressed":1,"count":2})) + self.event_source.on_next(MicroSquadEvent(EventType.BUTTON, payload={"pressed":0,"count":2})) + time.sleep(0.5) + self.event_source.on_next(MicroSquadEvent(EventType.GAME_TRANSITION, payload="get_events")) + time.sleep(0.5) + self.event_source.on_next(MicroSquadEvent(EventType.BUTTON, payload={"pressed":1,"count":1})) + self.event_source.on_next(MicroSquadEvent(EventType.BUTTON, payload={"pressed":1,"count":2})) + self.event_source.on_next(MicroSquadEvent(EventType.BUTTON, payload={"pressed":0,"count":2})) + time.sleep(0.5) + assert len(test_game.received_events)==3 + + + + + + + + + +if __name__ == '__main__': + unittest.main() diff --git a/modules/gateway/src/test/python/microsquad/mapper/homie/gateway/test_gateway_devices.py b/modules/gateway/src/test/python/microsquad/mapper/homie/gateway/test_gateway_devices.py new file mode 100644 index 0000000..fdd6fcc --- /dev/null +++ b/modules/gateway/src/test/python/microsquad/mapper/homie/gateway/test_gateway_devices.py @@ -0,0 +1,61 @@ +from microsquad.event import MicroSquadEvent,EventType +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway + +import unittest + +from rx3.subject import Subject + +import logging +logging.getLogger('homie').setLevel(logging.WARN) + +class TestGatewayDevice(unittest.TestCase): + def setUp(self): + mqtt_settings = { + 'MQTT_BROKER' : 'localhost', + 'MQTT_PORT' : 1883, + } + self.event_source = Subject() + self.gateway = DeviceGateway(event_source=self.event_source,mqtt_settings=mqtt_settings) + + def test_add_player(self): + self.gateway._player_manager.add_player("01") + assert self.gateway.get_node("player-01") is not None + assert self.gateway.get_node("player-02") is None + self.gateway._player_manager.add_player("02") + assert self.gateway.get_node("player-02") is not None + assert self.gateway.get_node("player-02").get_property("nickname") is not None + assert self.gateway.get_node("player-02").get_property("skin") is not None + assert self.gateway.get_node("player-02").get_property("order") is not None + assert self.gateway.get_node("player-01").get_property("order").value == 0 + assert self.gateway.get_node("player-02").get_property("order").value == 1 + + def test_add_remove_teams(self): + self.gateway._team_manager.add_team("blue") + assert self.gateway._team_manager.get_property("list").value == '["blue"]' + self.gateway._team_manager.remove_team("blue") + assert self.gateway._team_manager.get_property("list").value == '[]' + + def test_add_remove_player_to_team(self): + self.gateway._player_manager.add_player("susan") + self.gateway._player_manager.add_player("roger") + self.gateway._team_manager.add_team("orange") + assert self.gateway.get_node("team-orange") is not None + self.gateway._team_manager.add_player("orange:susan") + assert self.gateway._team_manager.get_property("list").value == '["orange"]' + assert self.gateway._team_manager.get_property("list-players").value == '{"orange":["susan"]}' + self.gateway._team_manager.add_player("orange:roger") + assert self.gateway._team_manager.get_property("list-players").value == '{"orange":["susan","roger"]}' + self.gateway._team_manager.remove_player("orange:susan") + assert self.gateway._team_manager.get_property("list-players").value == '{"orange":["roger"]}' + + def test_broadcast_event(self): + received_events: MicroSquadEvent = [] + subscriber = self.event_source.subscribe(on_next = lambda evt: received_events.append(evt) ) + self.gateway.update_broadcast("buttons") + assert 1 == len(received_events) + assert EventType.TERMINAL_BROADCAST == received_events[0].event_type + assert received_events[0].device_id is None + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/modules/gateway/src/test/python/microsquad/mapper/homie/gateway/test_terminal_devices.py b/modules/gateway/src/test/python/microsquad/mapper/homie/gateway/test_terminal_devices.py new file mode 100644 index 0000000..deb53a7 --- /dev/null +++ b/modules/gateway/src/test/python/microsquad/mapper/homie/gateway/test_terminal_devices.py @@ -0,0 +1,39 @@ +from microsquad.mapper.homie.terminal.device_terminal import DeviceTerminal + +import unittest + +from rx3.subject import Subject + +from microsquad.event import EventType + +import logging +logging.getLogger('homie').setLevel(logging.WARN) + +class TestTerminalDevice(unittest.TestCase): + def setUp(self): + self.mqtt_settings = { + 'MQTT_BROKER' : 'localhost', + 'MQTT_PORT' : 1883, + } + self.received_events = [] + self.terminals: DeviceTerminal = [] + self._event_source = Subject() + self.terminals.append(DeviceTerminal(device_id="terminal-01",name="Terminal 01",event_source=self._event_source, mqtt_settings=self.mqtt_settings)) + + + def test_button_a(self): + self.terminals[0].get_node("button-a").get_property("pressed").value = True + assert self.terminals[0].get_node("button-a").get_property("pressed").value + + def test_terminal_command_event(self): + subscriber = self._event_source.subscribe(on_next = lambda evt: self.received_events.append(evt) ) + command_string = "vote,image=99999" + self.terminals[0].update_command(command_string) + assert 1 == len(self.received_events) + assert EventType.TERMINAL_COMMAND == self.received_events[0].event_type + assert self.received_events[0].device_id is None + assert command_string == self.received_events[0].payload + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/modules/gateway/src/test/python/microsquad/mapper/homie/test_homie_mapper.py b/modules/gateway/src/test/python/microsquad/mapper/homie/test_homie_mapper.py new file mode 100644 index 0000000..0ba0d1c --- /dev/null +++ b/modules/gateway/src/test/python/microsquad/mapper/homie/test_homie_mapper.py @@ -0,0 +1,47 @@ +import unittest +from microsquad.event import EventType, MicroSquadEvent + +import rx3 +from microsquad.mapper.homie.homie_mapper import HomieMapper +from microsquad.mapper.homie.gateway.device_gateway import DeviceGateway +from rx3.subject import Subject + +import logging +logging.getLogger('homie').setLevel(logging.WARN) + +class TestHomieMapper(unittest.TestCase): + def setUp(self): + _mqtt_settings = { + 'MQTT_BROKER' : 'localhost', + 'MQTT_PORT' : 1883, + } + self.received_events: MicroSquadEvent = [] + self.event_source = Subject() + self.gateway = DeviceGateway(event_source = self.event_source,mqtt_settings=_mqtt_settings) + + self.mapper = HomieMapper(self.gateway,self.event_source) + self.subscriber = self.event_source.subscribe(on_next = lambda evt: self.received_events.append(evt) ) + + self.gateway.start() + + def test_bonjour_event(self): + dev_id = "12345678" + self.mapper.map_from_microbit('bonjour,dev_id={}'.format(dev_id)) + bonjour_events = list(filter(lambda evt: evt.event_type == EventType.BONJOUR, self.received_events)) + assert 1 == len(bonjour_events) + assert dev_id == bonjour_events[0].device_id + + def test_read_accelerator_event(self): + dev_id = "1234-5678" + readings = {'x':-12,'y':80,'z':-60} + self.mapper.map_from_microbit('read_accel,x={x},y={y},z={z},dev_id="{0}"'.format(dev_id,**readings)) + accel_events = list(filter(lambda evt: evt.event_type == EventType.ACCELERATOR, self.received_events)) + assert 1 == len(accel_events) + for evt in accel_events: + assert dev_id == evt.device_id + for k in readings: + assert readings[k] == int(accel_events[0].payload[k]) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/modules/gateway/src/test/python/microsquad/mapper/test_protocol_parser.py b/modules/gateway/src/test/python/microsquad/mapper/test_protocol_parser.py new file mode 100644 index 0000000..04b80e3 --- /dev/null +++ b/modules/gateway/src/test/python/microsquad/mapper/test_protocol_parser.py @@ -0,0 +1,27 @@ +import unittest + +from microsquad.mapper.line_protocol_parser import LineProtocolParser +class TestLineProtocolParser(unittest.TestCase): + + def setUp(self) -> None: + self.parser = LineProtocolParser() + return super().setUp() + + def test_simple_line(self): + msg = self.parser.parse('measurement,tag=value field=12345423 123') + expected = ('measurement',dict(tag='value'),dict(field=12345423),123) + self.assertEqual(msg, expected) + + def test_no_fields(self): + msg = self.parser.parse('measurement,tag=value 1235') + expected = ('measurement',dict(tag='value'),dict(),1235) + self.assertEqual(msg, expected) + + def test_no_tags(self): + msg = self.parser.parse('measurement 1238978') + expected = ('measurement',dict(),dict(),1238978) + self.assertEqual(msg, expected) + + +if __name__ == '__main__': + unittest.main() diff --git a/modules/web-ui/.ci/mosquitto.conf b/modules/web-ui/.ci/mosquitto.conf new file mode 100644 index 0000000..35a9cf6 --- /dev/null +++ b/modules/web-ui/.ci/mosquitto.conf @@ -0,0 +1,5 @@ +listener 1883 +protocol mqtt + +listener 9001 +protocol websockets \ No newline at end of file diff --git a/modules/web-ui/.dockerignore b/modules/web-ui/.dockerignore new file mode 100644 index 0000000..359bef4 --- /dev/null +++ b/modules/web-ui/.dockerignore @@ -0,0 +1,14 @@ +node_modules +.ci +.git +.gitignore +.github +.env +README.md +sonar-project.properties +cypress +cypress.json + +Dockerfile +.dockerignore +docker-compose.yml \ No newline at end of file diff --git a/modules/web-ui/.gitignore b/modules/web-ui/.gitignore new file mode 100644 index 0000000..ce34409 --- /dev/null +++ b/modules/web-ui/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + + +# dotenv environment variables file +.env + +# webpack generate output +dist + +# cypress output +cypress/videos/* +cypress/screenshots/* \ No newline at end of file diff --git a/modules/web-ui/Dockerfile b/modules/web-ui/Dockerfile new file mode 100644 index 0000000..1f50c3a --- /dev/null +++ b/modules/web-ui/Dockerfile @@ -0,0 +1,57 @@ +FROM node:14-alpine3.13 as builder + +RUN apk add --update nodejs npm + +RUN mkdir /home/node/app && chown -R node:node /home/node/app + +WORKDIR /home/node/app + +# Cache node modules first +COPY --chown=node:node package*.json ./ + +USER node + +RUN npm install --only=prod + +COPY --chown=node:node . . + +RUN npm run build + +# Fix asset loading +RUN mv ./public/assets ./dist/ +RUN mv ./public/conf ./dist/ + +FROM nginx:stable-alpine + +RUN apk update + +###################### +# Make the image Openshift-friendly +RUN chmod g+rwxt /var/cache/nginx /var/run /var/log/nginx /etc/nginx/conf.d || true +# Remove the upstream default configuration +RUN rm /etc/nginx/conf.d/default.conf +# Remove the upstream IPv6 configuration entrypoint - only useful if we kept the default configuration +RUN rm /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh +RUN chgrp -R root /var/cache/nginx + +# comment user directive as master process is run as user in OpenShift anyhow +RUN sed -i.bak 's/^user/#user/' /etc/nginx/nginx.conf + +RUN addgroup nginx root + +COPY --from=builder /home/node/app/dist /usr/share/nginx/html + +ENV NGINX_PORT=8080 +ENV NGINX_HOSTNAME=localhost +# You can override with your context path, without leading and trailing slashes +# e.g. /web-ui +ENV NGINX_CONTEXT_PATH=/web-ui + +COPY deployment/docker/etc/nginx /etc/nginx + +# # You can inject the JSON configuration file as config.json in this location +VOLUME /usr/share/nginx/html/conf/ + +# nginx default.conf.template +VOLUME "/etc/nginx/templates" +USER nginx diff --git a/modules/web-ui/README.md b/modules/web-ui/README.md new file mode 100644 index 0000000..096fe0a --- /dev/null +++ b/modules/web-ui/README.md @@ -0,0 +1,56 @@ +# MicroSquad Web UI + +The MicroSquad web UI relies on a MQTT broker. + +## How to develop the frontend + +### Using the maven frontend plugin (preferred method) + +* Install Apache Maven 3.6.3+ +* Run ```mvn compile``` - this will download and install a local version of Node and NPM +* To run the project from the command line : + * Execute ```. source-path.sh``` to update your path (on a non-Linux platform, amend the path as indicated in the script) + * Execute ```npm start``` to run the project + +### Using a global node installation + +* Install Node v14 or later and NPM v6.14 or later +* Execute ```npm start``` + +## How to deploy on Openshift + +### Preparation steps + +* Login to Openshift and switch to your project +* Create a service account for deployment + * oc create sa usquad-deployer + * oc policy add-role-to-user admin -z usquad-deployer +* You can now obtain the auth token for that account and use it in your build + * oc sa get-token usquad-deployer + +### Deploy commands + + +```bash +oc process -p NAMESPACE=microsquad -f deployment/service.yml --local=true | oc apply -f - +``` + +## How to develop locally with Docker + +* Create the image + ```bash + docker build -t usquad . + ``` +* Start the image + ```bash + docker run -it --rm --name usquad -e NGINX_PORT=8080 -e NGINX_CONTEXT_PATH=/ui -v `pwd`/deployment/conf/nginx/templates:/etc/nginx/templates -p 8080:8080 usquad + ``` +* Access the server from your web browser at http://localhost:8080/ui +## How to set a background image on the Scoreboard + +Simply post a base-64 encoded image via mqtt like so : +``` +mosquitto_pub -r -t "microsquad/gateway/scoreboard/image" -m "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAJCAYAAAA7KqwyAAAAF0lEQVR42mP8z8BwhoECwDhqwKgBQAAAZaoQLT5kb68AAAAASUVORK5CYII=" +``` +The website [PNG Pixel](https://png-pixel.com/) is a great help ! + diff --git a/modules/web-ui/cypress.json b/modules/web-ui/cypress.json new file mode 100644 index 0000000..941f077 --- /dev/null +++ b/modules/web-ui/cypress.json @@ -0,0 +1,3 @@ +{ + "baseUrl": "http://localhost:8000/" +} diff --git a/modules/web-ui/cypress/integration/spec.js b/modules/web-ui/cypress/integration/spec.js new file mode 100644 index 0000000..7e84d54 --- /dev/null +++ b/modules/web-ui/cypress/integration/spec.js @@ -0,0 +1,44 @@ +describe('Usquad UI Test', () => { + beforeEach(() => { + cy.visit('/'); + cy.wait(100); + }) + + it('Loads player models', () => { + cy.get('#add-player').click(); + }) + + it('Can run player commands', () => { + cy.publish('players/playerA', 'add'); + + cy.publish('players/playerA', 'skin,alienA'); + + cy.publish('players/playerA', 'accessory,cap'); + + cy.publish('players/playerA', 'animation,Run'); + + cy.publish('players/playerA', 'say,Hello!'); + }) + + it('Can run team commands', () => { + cy.publish('players/playerA', 'add'); + + cy.publish('players/playerB', 'add'); + + cy.publish('players/playerC', 'add'); + + cy.publish('players/playerA', 'team,teamA'); + + cy.publish('players/playerB', 'team,teamB'); + + cy.publish('players/playerC', 'team,teamA'); + + cy.publish('teams/teamA', 'animation,Run'); + + cy.publish('teams/teamB', 'animation,CrouchIdle'); + + cy.publish('teams', 'reset'); + + cy.publish('teams', 'split,team1,team2') + }) +}) \ No newline at end of file diff --git a/modules/web-ui/cypress/plugins/index.js b/modules/web-ui/cypress/plugins/index.js new file mode 100644 index 0000000..aa9918d --- /dev/null +++ b/modules/web-ui/cypress/plugins/index.js @@ -0,0 +1,21 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/modules/web-ui/cypress/support/commands.js b/modules/web-ui/cypress/support/commands.js new file mode 100644 index 0000000..33128ca --- /dev/null +++ b/modules/web-ui/cypress/support/commands.js @@ -0,0 +1,5 @@ +Cypress.Commands.add("publish", (topic, message) => { + cy.get('#pub-topic').clear().type(topic); + cy.get('#pub-payload').clear().type(message); + cy.get('#publish-button').click(); +}); \ No newline at end of file diff --git a/modules/web-ui/cypress/support/index.js b/modules/web-ui/cypress/support/index.js new file mode 100644 index 0000000..d68db96 --- /dev/null +++ b/modules/web-ui/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/modules/web-ui/deployment/cern-k8s/mosquitto/mosquitto-deployment.yaml b/modules/web-ui/deployment/cern-k8s/mosquitto/mosquitto-deployment.yaml new file mode 100644 index 0000000..535cd18 --- /dev/null +++ b/modules/web-ui/deployment/cern-k8s/mosquitto/mosquitto-deployment.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: Service +metadata: + name: mosquitto-service +spec: + type: ClusterIP + ports: + - port: 9001 + protocol: TCP + name: websockets + selector: + app: mosquitto +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mosquitto-config +data: + mosquitto.conf: |- + listener 9001 + protocol websockets +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mosquitto +spec: + selector: + matchLabels: + app: mosquitto + replicas: 1 + template: + metadata: + labels: + app: mosquitto + spec: + containers: + - name: mosquitto + image: eclipse-mosquitto:1.6 + ports: + - containerPort: 9001 + protocol: TCP + volumeMounts: + - mountPath: /mosquitto/config + name: mosquitto-config-volume + volumes: + - name: mosquitto-config-volume + configMap: + name: mosquitto-config diff --git a/modules/web-ui/deployment/cern-k8s/mosquitto/traefik-mqtt.yaml b/modules/web-ui/deployment/cern-k8s/mosquitto/traefik-mqtt.yaml new file mode 100644 index 0000000..f129ab5 --- /dev/null +++ b/modules/web-ui/deployment/cern-k8s/mosquitto/traefik-mqtt.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/frontend-entry-points: http + traefik.ingress.kubernetes.io/rewrite-target: /$1 + name: traefik-mosquitto +spec: + rules: + - host: usquad.cern.ch + http: + paths: + - backend: + service: + name: mosquitto-service + port: + number: 9001 + path: /mqtt + pathType: Prefix diff --git a/modules/web-ui/deployment/cern-k8s/usquad/traefik-ui.yaml b/modules/web-ui/deployment/cern-k8s/usquad/traefik-ui.yaml new file mode 100644 index 0000000..83d7b7c --- /dev/null +++ b/modules/web-ui/deployment/cern-k8s/usquad/traefik-ui.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/frontend-entry-points: http + name: traefik +spec: + rules: + - host: usquad.cern.ch + http: + paths: + - backend: + service: + name: usquad-service + port: + number: 8080 + path: / + pathType: Prefix diff --git a/modules/web-ui/deployment/cern-k8s/usquad/usquad-deployment.yaml b/modules/web-ui/deployment/cern-k8s/usquad/usquad-deployment.yaml new file mode 100644 index 0000000..9283644 --- /dev/null +++ b/modules/web-ui/deployment/cern-k8s/usquad/usquad-deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: Service +metadata: + name: usquad-service +spec: + type: ClusterIP + ports: + - port: 8080 + protocol: TCP + name: http + selector: + app: usquad +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-templates-config +data: + default.conf.template: |- + server{ + listen 8080; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: usquad-config +data: + config.json: |- + { + "MQTT_URI": "ws://usquad.cern.ch/mqtt", + "MQTT_CLIENT_ID": "client-id" + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: usquad +spec: + selector: + matchLabels: + app: usquad + replicas: 3 + template: + metadata: + labels: + app: usquad + spec: + containers: + - name: usquad-web-ui + image: ghcr.io/cmcrobotics/microsquad-web-ui:latest + ports: + - containerPort: 8080 + protocol: TCP + volumeMounts: + - name: nginx-templates-config-volume + mountPath: /etc/nginx/templates + - name: usquad-config-volume + mountPath: /usr/share/nginx/html/conf + volumes: + - name: nginx-templates-config-volume + configMap: + name: nginx-templates-config + - name: usquad-config-volume + configMap: + name: usquad-config diff --git a/modules/web-ui/deployment/cern-oc/master.env b/modules/web-ui/deployment/cern-oc/master.env new file mode 100644 index 0000000..2f18275 --- /dev/null +++ b/modules/web-ui/deployment/cern-oc/master.env @@ -0,0 +1,4 @@ +OPENSHIFT_SERVER="https://openshift.cern.ch" +NAMESPACE=microsquad +MQTT_URI="wss://microsquad.web.cern.ch:9001/" +IMAGE_VERSION=master \ No newline at end of file diff --git a/modules/web-ui/deployment/cern-oc/routes.yml b/modules/web-ui/deployment/cern-oc/routes.yml new file mode 100644 index 0000000..b997f4e --- /dev/null +++ b/modules/web-ui/deployment/cern-oc/routes.yml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: Template +metadata: + name: web-ui-service-template +objects: + - apiVersion: route.openshift.io/v1 + kind: Route + metadata: + labels: + app: usquad + annotations: + router.cern.ch/network-visibility: Internet + name: web-ui-route + namespace: ${NAMESPACE} + spec: + host: ${NAMESPACE}.web.cern.ch + #path: ${CONTEXT_PATH} + port: + targetPort: 8080-tcp + tls: + insecureEdgeTerminationPolicy: Redirect + termination: edge + to: + kind: Service + name: web-ui + weight: 100 + wildcardPolicy: None + - apiVersion: route.openshift.io/v1 + kind: Route + metadata: + labels: + app: usquad + annotations: + router.cern.ch/network-visibility: Internet + name: mosquitto-websocket-route + namespace: ${NAMESPACE} + spec: + host: ${NAMESPACE}.web.cern.ch + path: /mqtt + port: + targetPort: 9001-tcp + tls: + insecureEdgeTerminationPolicy: Redirect + termination: edge + to: + kind: Service + name: mosquitto + weight: 100 + wildcardPolicy: None + +parameters: + - name: NAMESPACE + description: Website namespace (hostname) + required: true + - name: CONTEXT_PATH + description: Web UI Context Path + value: "/ui" diff --git a/modules/web-ui/deployment/cern-oc/service.yml b/modules/web-ui/deployment/cern-oc/service.yml new file mode 100644 index 0000000..fc55755 --- /dev/null +++ b/modules/web-ui/deployment/cern-oc/service.yml @@ -0,0 +1,317 @@ +apiVersion: v1 +kind: Template +metadata: + name: web-ui-service-template +objects: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: usquad + name: web-ui + namespace: ${NAMESPACE} + selfLink: /api/v1/namespaces/${NAMESPACE}/services/web-ui + spec: + ports: + - name: 8080-tcp + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: web-ui-dc + sessionAffinity: None + type: ClusterIP + - apiVersion: route.openshift.io/v1 + kind: Route + metadata: + labels: + app: usquad + annotations: + router.cern.ch/network-visibility: Internet + name: web-ui-route + namespace: ${NAMESPACE} + spec: + host: ${NAMESPACE}.web.cern.ch + path: ${CONTEXT_PATH} + port: + targetPort: 8080-tcp + tls: + insecureEdgeTerminationPolicy: Redirect + termination: edge + to: + kind: Service + name: web-ui + weight: 100 + wildcardPolicy: None + - apiVersion: route.openshift.io/v1 + kind: Route + metadata: + labels: + app: usquad + annotations: + router.cern.ch/network-visibility: Internet + name: mosquitto-websocket-route + namespace: ${NAMESPACE} + spec: + host: ${NAMESPACE}.web.cern.ch + path: /mqtt + port: + targetPort: 9001-tcp + tls: + insecureEdgeTerminationPolicy: Redirect + termination: edge + to: + kind: Service + name: mosquitto + weight: 100 + wildcardPolicy: None + - apiVersion: v1 + kind: Service + metadata: + labels: + app: usquad + name: mosquitto + namespace: ${NAMESPACE} + selfLink: /api/v1/namespaces/${NAMESPACE}/services/mosquitto + spec: + ports: + - name: 9001-tcp + port: 9001 + protocol: TCP + targetPort: 9001 + selector: + deploymentconfig: mosquitto-dc + sessionAffinity: None + type: ClusterIP + - kind: ConfigMap + apiVersion: v1 + metadata: + name: mosquitto-config + namespace: ${NAMESPACE} + data: + mosquitto.conf: |- + # Config file for mosquitto + retry_interval 20 + sys_interval 10 + max_inflight_messages 40 + max_queued_messages 200 + queue_qos0_messages false + message_size_limit 0 + allow_zero_length_clientid true + allow_duplicate_messages false + # Logging + connection_messages true + log_dest stderr + log_dest stdout + # log_dest file /mosquitto/log/mosquitto.log + log_type error + log_type warning + log_type notice + log_type information + log_type all + log_type debug + log_timestamp true + + listener 1883 + protocol mqtt + + listener 9001 + protocol websockets + - apiVersion: apps.openshift.io/v1 + kind: DeploymentConfig + metadata: + labels: + app: usquad + name: mosquitto-dc + namespace: ${NAMESPACE} + selfLink: >- + /apis/apps.openshift.io/v1/namespaces/${NAMESPACE}/deploymentconfigs/mosquitto + spec: + replicas: 1 + selector: + app: usquad + deploymentconfig: mosquitto-dc + strategy: + recreateParams: + timeoutSeconds: 240 + resources: + requests: + cpu: 100m + memory: 200Mi + type: Recreate + template: + metadata: + labels: + app: usquad + deploymentconfig: mosquitto-dc + spec: + containers: + - image: >- + eclipse-mosquitto:1.6 + imagePullPolicy: IfNotPresent + name: mosquitto + ports: + - containerPort: 9001 + protocol: TCP + resources: + requests: + cpu: 200m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /mosquitto/config/ + name: mosquitto-config-volume + readOnly: true + volumes: + - name: mosquitto-config-volume + configMap: + name: mosquitto-config + - name: mosquitto-log + + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: { } + terminationGracePeriodSeconds: 30 + - kind: ConfigMap + apiVersion: v1 + metadata: + name: web-ui-config + namespace: ${NAMESPACE} + data: + config.json: |- + { + "MQTT_URI": "wss://${NAMESPACE}.web.cern.ch/mqtt", + "MQTT_CLIENT_ID":"web-ui-client" + } + - kind: ConfigMap + apiVersion: v1 + metadata: + name: nginx-templates-config + namespace: ${NAMESPACE} + data: + default.conf.template: |- + server{ + listen ${NGINX_PORT} default_server; + server_name ${NGINX_HOSTNAME}; + root /usr/share/nginx/html; + + location = ${NGINX_CONTEXT_PATH} { + rewrite ^ ${NGINX_CONTEXT_PATH}/index.html permanent; + } + + location ~ ${NGINX_CONTEXT_PATH}/.* { + rewrite ^${NGINX_CONTEXT_PATH}/(.*)$ /$1 last; + index index.html index.htm; + + } + } + - apiVersion: apps.openshift.io/v1 + kind: DeploymentConfig + metadata: + labels: + app: usquad + name: web-ui-dc + namespace: ${NAMESPACE} + selfLink: >- + /apis/apps.openshift.io/v1/namespaces/${NAMESPACE}/deploymentconfigs/web-ui + spec: + replicas: 1 + selector: + app: usquad + deploymentconfig: web-ui-dc + strategy: + recreateParams: + timeoutSeconds: 240 + resources: + requests: + cpu: 100m + memory: 200Mi + type: Recreate + template: + metadata: + labels: + app: usquad + deploymentconfig: web-ui-dc + spec: + containers: + - image: >- + gitlab-registry.cern.ch/cmcrobotics/microsquad:${IMAGE_VERSION} + imagePullPolicy: Always + name: web-ui + livenessProbe: + failureThreshold: 5 + httpGet: + path: ${CONTEXT_PATH} + port: 8080 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 60 + successThreshold: 1 + timeoutSeconds: 5 + readinessProbe: + failureThreshold: 5 + httpGet: + path: ${CONTEXT_PATH} + port: 8080 + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 15 + successThreshold: 3 + timeoutSeconds: 15 + env: + - name: NGINX_CONTEXT_PATH + value: "${CONTEXT_PATH}" + - name: NGINX_PORT + value: "8080" + - name: NGINX_HOSTNAME + value: "${NAMESPACE}.web.cern.ch" + ports: + - containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: httpd-run-volume + mountPath: "/run" + - name: nginx-templates-config-volume + mountPath: "/etc/nginx/templates" + - name: web-ui-config-volume + mountPath: "/usr/share/nginx/html/conf" + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: { } + terminationGracePeriodSeconds: 30 + volumes: + - name: httpd-run-volume + emptyDir: {} + - name: web-ui-config-volume + configMap: + name: web-ui-config + - name: nginx-templates-config-volume + configMap: + name: nginx-templates-config + +parameters: + - name: NAMESPACE + description: Website namespace (hostname) + required: true + - name: CONTEXT_PATH + description: Web UI Context Path + value: "/ui" + - name: IMAGE_VERSION + description: Web UI image version + value: develop + diff --git a/modules/web-ui/deployment/docker/etc/nginx/templates/default.conf.template b/modules/web-ui/deployment/docker/etc/nginx/templates/default.conf.template new file mode 100644 index 0000000..d11cc3e --- /dev/null +++ b/modules/web-ui/deployment/docker/etc/nginx/templates/default.conf.template @@ -0,0 +1,15 @@ +server{ + listen ${NGINX_PORT} default_server; + server_name ${NGINX_HOSTNAME}; + root /usr/share/nginx/html; + + location = ${NGINX_CONTEXT_PATH} { + rewrite ^ ${NGINX_CONTEXT_PATH}/index.html permanent; + } + + location ~ ${NGINX_CONTEXT_PATH}/.* { + rewrite ^${NGINX_CONTEXT_PATH}/(.*)$ /$1 last; + index index.html index.htm; + + } +} \ No newline at end of file diff --git a/modules/web-ui/deployment/gke/service.yml b/modules/web-ui/deployment/gke/service.yml new file mode 100644 index 0000000..857b392 --- /dev/null +++ b/modules/web-ui/deployment/gke/service.yml @@ -0,0 +1,154 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: usquad-ingress + annotations: + # Use nginx ingress controller to use rewrites + # Installation guide: https://kubernetes.github.io/ingress-nginx/deploy/ + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/rewrite-target: /$1 + # Increase timeout for websocket connections + # https://kubernetes.github.io/ingress-nginx/user-guide/miscellaneous/#websockets + # even though web-ui will attempt reconnect on connection close, + # ensure timeout is higher than MQTT broker keepAlive + nginx.ingress.kubernetes.io/proxy-read-timeout: "120" + nginx.ingress.kubernetes.io/proxy-send-timeout: "120" +spec: + rules: + - host: 35.189.115.151.nip.io + http: + paths: + - path: /ui/(.*) + backend: + serviceName: usquad-internal-service + servicePort: 8080 + - path: /mqtt + backend: + serviceName: mosquitto-internal-service + servicePort: 9001 +--- +apiVersion: v1 +kind: Service +metadata: + name: usquad-internal-service +spec: + type: ClusterIP + ports: + - port: 8080 + protocol: TCP + name: http + selector: + run: usquad +--- +apiVersion: v1 +kind: Service +metadata: + name: mosquitto-internal-service +spec: + type: ClusterIP + ports: + - port: 9001 + protocol: TCP + name: websockets + selector: + run: mosquitto +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-templates-config +data: + default.conf.template: |- + server{ + listen 8080; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mosquitto-config +data: + mosquitto.conf: |- + listener 9001 + protocol websockets +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: web-ui-config +data: + config.json: |- + { + "MQTT_URI": "ws://35.189.115.151.nip.io/mqtt", + "MQTT_CLIENT_ID": "web-ui-client" + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mosquitto +spec: + selector: + matchLabels: + run: mosquitto + replicas: 1 + template: + metadata: + labels: + run: mosquitto + spec: + containers: + - name: mosquitto + image: eclipse-mosquitto:1.6 + ports: + - containerPort: 9001 + protocol: TCP + volumeMounts: + - name: mosquitto-config-volume + mountPath: /mosquitto/config + volumes: + - name: mosquitto-config-volume + configMap: + name: mosquitto-config +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: usquad +spec: + selector: + matchLabels: + run: usquad + replicas: 1 + template: + metadata: + labels: + run: usquad + spec: + containers: + - name: usquad-web-ui + image: ghcr.io/cmcrobotics/microsquad-web-ui:latest + ports: + - containerPort: 8080 + protocol: TCP + volumeMounts: + - name: nginx-templates-config-volume + mountPath: /etc/nginx/templates + - name: web-ui-config-volume + mountPath: /usr/share/nginx/html/conf + volumes: + - name: nginx-templates-config-volume + configMap: + name: nginx-templates-config + - name: web-ui-config-volume + configMap: + name: web-ui-config diff --git a/modules/web-ui/entrypoint.sh b/modules/web-ui/entrypoint.sh new file mode 100644 index 0000000..d4dd46f --- /dev/null +++ b/modules/web-ui/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -xe + +# Ensure environment vars are set +: "${MQTT_URI?MQTT URI environment var was not set}" +: "${MQTT_TOPIC_ROOT?MQTT Topic Root was not set}" + +# Replace them in bundle.js +sed -i "s/MQTT_URI_REPLACE/$MQTT_URI/g" /var/www/localhost/htdocs/js/bundle.js +sed -i "s/MQTT_TOPIC_ROOT_REPLACE/$MQTT_TOPIC_ROOT/g" /var/www/localhost/htdocs/js/bundle.js + +exec "$@" \ No newline at end of file diff --git a/modules/web-ui/package-lock.json b/modules/web-ui/package-lock.json new file mode 100644 index 0000000..8c86a04 --- /dev/null +++ b/modules/web-ui/package-lock.json @@ -0,0 +1,5223 @@ +{ + "name": "usquad-web-ui", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@popperjs/core": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", + "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==" + }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==" + }, + "@types/eslint": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz", + "integrity": "sha512-I+1sYH+NPQ3/tVqCeUSBwTE/0heyvtXqpIopUUArlBm0Kpocb8FbMa3AZ/ASKIFpN3rnEx932TTXDbt9OXsNDw==", + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz", + "integrity": "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==", + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.46", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz", + "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==" + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==" + }, + "@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + }, + "@types/node": { + "version": "14.14.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", + "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==" + }, + "@types/tapable": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", + "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==" + }, + "@types/uglify-js": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.11.1.tgz", + "integrity": "sha512-7npvPKV+jINLu1SpSYVWG8KvyJBhBa8tmzMMdDoVc2pWUYHN8KIXlPJhjJ4LT97c4dXJA2SHL/q6ADbDriZN+Q==", + "requires": { + "source-map": "^0.6.1" + } + }, + "@types/webpack": { + "version": "4.41.26", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.26.tgz", + "integrity": "sha512-7ZyTfxjCRwexh+EJFwRUM+CDB2XvgHl4vfuqf1ZKrgGvcS5BrNvPQqJh3tsZ0P6h6Aa1qClVHaJZszLPzpqHeA==", + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + } + }, + "@types/webpack-sources": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz", + "integrity": "sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg==", + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "@webassemblyjs/ast": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", + "integrity": "sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==", + "requires": { + "@webassemblyjs/helper-numbers": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz", + "integrity": "sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz", + "integrity": "sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz", + "integrity": "sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==" + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz", + "integrity": "sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==", + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz", + "integrity": "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz", + "integrity": "sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz", + "integrity": "sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==", + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.0.tgz", + "integrity": "sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==", + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.0.tgz", + "integrity": "sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz", + "integrity": "sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/helper-wasm-section": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-opt": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "@webassemblyjs/wast-printer": "1.11.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz", + "integrity": "sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz", + "integrity": "sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz", + "integrity": "sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz", + "integrity": "sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz", + "integrity": "sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "bootstrap": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.1.tgz", + "integrity": "sha512-/jUa4sSuDZWlDLQ1gwQQR8uoYSvLJzDd8m5o6bPKh3asLAMYVZKdRCjb1joUd5WXf0WwCNzd2EjwQQhupou0dA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.1.tgz", + "integrity": "sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA==", + "requires": { + "caniuse-lite": "^1.0.30001173", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.634", + "escalade": "^3.1.1", + "node-releases": "^1.1.69" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "caniuse-lite": { + "version": "1.0.30001180", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001180.tgz", + "integrity": "sha512-n8JVqXuZMVSPKiPiypjFtDTXc4jWIdjxull0f92WLo7e1MSi3uJ3NvveakSh/aCl1QKFAvIz3vIj0v+0K+FrXw==" + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "requires": { + "source-map": "~0.6.0" + } + }, + "clean-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", + "requires": { + "@types/webpack": "^4.4.31", + "del": "^4.1.1" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "css-loader": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.3.0.tgz", + "integrity": "sha512-9NGvHOR+L6ps13Ilw/b216++Q8q+5RpJcVufCdW9S/9iCzs4KBDNa8qnA/n3FK/sSfWmH35PAIK/cfPi7LOSUg==", + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + } + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", + "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==" + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "dotenv-defaults": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.1.tgz", + "integrity": "sha512-ugFCyBF7ILuwpmznduHPQZBMucHHJ8T4OBManTEVjemxCm2+nqifSuW2lD2SNKdiKSH1E324kZSdJ8M04b4I/A==", + "requires": { + "dotenv": "^8.2.0" + } + }, + "dotenv-webpack": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-6.0.4.tgz", + "integrity": "sha512-WiTPNLanDNJ1O8AvgkBpsbarw78a4PMYG2EfJcQoxTHFWy+ji213HR+3f4PhWB1RBumiD9cbiuC3SNxJXbBp9g==", + "requires": { + "dotenv-defaults": "^2.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "electron-to-chromium": { + "version": "1.3.645", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.645.tgz", + "integrity": "sha512-T7mYop3aDpRHIQaUYcmzmh6j9MAe560n6ukqjJMbVC6bVTau7dSpvB18bcsBPPtOSe10cKxhJFtlbEzLa0LL1g==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "requires": { + "prr": "~1.0.1" + } + }, + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + }, + "dependencies": { + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + } + } + }, + "es-module-lexer": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz", + "integrity": "sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==" + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.0.tgz", + "integrity": "sha512-M11rgtQp5GZMZzDL7jLTNxbDfurpzuau5uqRWDPvlHjfvg3TdScAZo96GLvhMjImrmR8uAt0FS2RLoMrfWGKlg==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "requires": { + "global-prefix": "^3.0.0" + }, + "dependencies": { + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + } + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", + "dev": true + }, + "html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "requires": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + } + } + }, + "html-webpack-plugin": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", + "integrity": "sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==", + "requires": { + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.20", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", + "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", + "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==" + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==" + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==" + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", + "dev": true + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" + }, + "mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "requires": { + "mime-db": "1.45.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, + "nanocolors": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.12.tgz", + "integrity": "sha512-SFNdALvzW+rVlzqexid6epYdt8H9Zol7xDoQarioEFcFN0JHo4CYNztAxmtfgGTVRCmFlEOqqhBpoFGKqSAMug==" + }, + "nanoid": { + "version": "3.1.28", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.28.tgz", + "integrity": "sha512-gSu9VZ2HtmoKYe/lmyPFES5nknFrHa+/DT9muUFWFMi6Jh9E1I7bkvlQ8xxf1Kos9pi9o8lBnIOkatMhKX/YUw==" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true + }, + "node-releases": { + "version": "1.1.70", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz", + "integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", + "integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "paho-mqtt": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/paho-mqtt/-/paho-mqtt-1.1.0.tgz", + "integrity": "sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==" + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "requires": { + "find-up": "^5.0.0" + } + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "postcss": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.8.tgz", + "integrity": "sha512-GT5bTjjZnwDifajzczOC+r3FI3Cu+PgPvrsjhQdRqa2kTJ4968/X9CUce9xttIB0xOs5c6xf0TCWZo/y9lF6bA==", + "requires": { + "nanocolors": "^0.2.2", + "nanoid": "^3.1.25", + "source-map-js": "^0.6.2" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + }, + "pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "requires": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz", + "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==", + "requires": { + "css-select": "^2.0.2", + "dom-converter": "^0.2", + "htmlparser2": "^3.10.1", + "lodash": "^4.17.20", + "strip-ansi": "^3.0.0" + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "dependencies": { + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + } + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "rxjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz", + "integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==", + "requires": { + "tslib": "~2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", + "dev": true, + "requires": { + "node-forge": "^0.10.0" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", + "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^3.4.0", + "websocket-driver": "^0.7.4" + } + }, + "sockjs-client": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.0.tgz", + "integrity": "sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "eventsource": "^1.0.7", + "faye-websocket": "^0.11.3", + "inherits": "^2.0.4", + "json3": "^3.3.3", + "url-parse": "^1.4.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "style-loader": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.0.tgz", + "integrity": "sha512-szANub7ksJtQioJYtpbWwh1hUl99uK15n5HDlikeCRil/zYMZgSxucHddyF/4A3qJMUiAjPhFowrrQuNMA7jwQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" + }, + "terser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz", + "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "terser-webpack-plugin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz", + "integrity": "sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q==", + "requires": { + "jest-worker": "^26.6.2", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "source-map": "^0.6.1", + "terser": "^5.5.1" + } + }, + "three": { + "version": "0.125.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.125.1.tgz", + "integrity": "sha512-7CbiSHZOc18ChhVZU8wQ2g9F2KHJqiG7+ND56/XMrJC2XZMmu+dZFeLFl380c5JwKZGHTOkBQzioZVkI7Jumhg==" + }, + "three-plain-animator": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/three-plain-animator/-/three-plain-animator-1.0.6.tgz", + "integrity": "sha512-goot3GYvr+2z/I/lO4Yczib2u9RdcqeE4Zv9F4AMUQ6cfm5EAUGlJ8L8fuKoi91Qjzkw0E3/M/nvs7Kp9Wuh1w==", + "dev": true, + "requires": { + "rxjs": "6.6.2", + "three": "0.120.0" + }, + "dependencies": { + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "three": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.120.0.tgz", + "integrity": "sha512-Swffpi3EAHWkmqC1MagKEzR5XgwkDiyeWI3M7vkGbBc0xhq2LcQmJj5DqBruLkrgcZQ+fM/+fSQBU1tDvggO4A==", + "dev": true + } + } + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "ts-loader": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.14.tgz", + "integrity": "sha512-Jt/hHlUnApOZjnSjTmZ+AbD5BGlQFx3f1D0nYuNKwz0JJnuDGHJas6az+FlWKwwRTu+26GXpv249A8UAnYUpqA==", + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" + }, + "unbox-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.0.tgz", + "integrity": "sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA==", + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.0", + "has-symbols": "^1.0.0", + "which-boxed-primitive": "^1.0.1" + } + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", + "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "watchpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.0.tgz", + "integrity": "sha512-UjgD1mqjkG99+3lgG36at4wPnUXNvis2v1utwTgQ43C22c4LD71LsYMExdWXh4HZ+RmW+B0t1Vrg2GpXAkTOQw==", + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webpack": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.18.0.tgz", + "integrity": "sha512-RmiP/iy6ROvVe/S+u0TrvL/oOmvP+2+Bs8MWjvBwwY/j82Q51XJyDJ75m0QAGntL1Wx6B//Xc0+4VPP/hlNHmw==", + "requires": { + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.46", + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/wasm-edit": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "acorn": "^8.0.4", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.7.0", + "es-module-lexer": "^0.3.26", + "eslint-scope": "^5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.4", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "pkg-dir": "^5.0.0", + "schema-utils": "^3.0.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.1", + "watchpack": "^2.0.0", + "webpack-sources": "^2.1.1" + }, + "dependencies": { + "enhanced-resolve": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", + "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "tapable": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", + "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==" + } + } + }, + "webpack-cli": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz", + "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==", + "requires": { + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.1.1", + "findup-sync": "^3.0.0", + "global-modules": "^2.0.0", + "import-local": "^2.0.0", + "interpret": "^1.4.0", + "loader-utils": "^1.4.0", + "supports-color": "^6.1.0", + "v8-compile-cache": "^2.1.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + } + } + }, + "webpack-dev-server": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", + "integrity": "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.8", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-merge": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", + "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==", + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.2.0.tgz", + "integrity": "sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==", + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } +} diff --git a/modules/web-ui/package.json b/modules/web-ui/package.json new file mode 100644 index 0000000..cab0fde --- /dev/null +++ b/modules/web-ui/package.json @@ -0,0 +1,37 @@ +{ + "name": "usquad-web-ui", + "version": "0.0.0", + "description": "", + "main": "webpack.prod.js", + "engines": { + "node": "14.15.4" + }, + "dependencies": { + "@popperjs/core": "^2.10.2", + "bootstrap": "^5.1.1", + "clean-webpack-plugin": "^3.0.0", + "css-loader": "^6.3.0", + "dotenv-webpack": "^6.0.4", + "express": "^4.17.1", + "html-webpack-plugin": "^4.5.2", + "paho-mqtt": "^1.1.0", + "rxjs": "^7.3.0", + "style-loader": "^3.3.0", + "three": "^0.125.1", + "ts-loader": "^8.0.14", + "typescript": "^4.1.3", + "webpack": "^5.18.0", + "webpack-cli": "^3.3.12", + "webpack-merge": "^5.7.3" + }, + "devDependencies": { + "three-plain-animator": "^1.0.6", + "webpack-dev-server": "^3.11.2" + }, + "scripts": { + "build": "webpack --config webpack.prod.js", + "start": "webpack-dev-server --port 8000 --config webpack.dev.js" + }, + "author": "Lucas Van Mol", + "license": "GPLv3" +} diff --git a/modules/web-ui/pom.xml b/modules/web-ui/pom.xml new file mode 100644 index 0000000..0cea9af --- /dev/null +++ b/modules/web-ui/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + com.github.cmcrobotics.microsquad + reactor + 0.1.0-SNAPSHOT + ../../pom.xml + + + microsquad-web-ui + jar + + Microsquad :: Web UI frontend + ${project.name} + + + + UTF-8 + UTF-8 + + + + + + com.github.eirslett + frontend-maven-plugin + 1.7.6 + + ${project.basedir} + ${node.version} + ${npm.version} + + + + install node and npm + + install-node-and-npm + + + + npm install + + npm + + + + npm run build + + npm + + + run build + + + + + + + + diff --git a/modules/web-ui/public/assets/accessories/astroBackpack.glb b/modules/web-ui/public/assets/accessories/astroBackpack.glb new file mode 100644 index 0000000..0f51c75 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/astroBackpack.glb differ diff --git a/modules/web-ui/public/assets/accessories/astroHelmet.glb b/modules/web-ui/public/assets/accessories/astroHelmet.glb new file mode 100644 index 0000000..4f95674 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/astroHelmet.glb differ diff --git a/modules/web-ui/public/assets/accessories/beard.glb b/modules/web-ui/public/assets/accessories/beard.glb new file mode 100644 index 0000000..1259ca7 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/beard.glb differ diff --git a/modules/web-ui/public/assets/accessories/cap.glb b/modules/web-ui/public/assets/accessories/cap.glb new file mode 100644 index 0000000..e66b840 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/cap.glb differ diff --git a/modules/web-ui/public/assets/accessories/farmerCap.glb b/modules/web-ui/public/assets/accessories/farmerCap.glb new file mode 100644 index 0000000..a85093d Binary files /dev/null and b/modules/web-ui/public/assets/accessories/farmerCap.glb differ diff --git a/modules/web-ui/public/assets/accessories/glassesRetro.glb b/modules/web-ui/public/assets/accessories/glassesRetro.glb new file mode 100644 index 0000000..e720d3c Binary files /dev/null and b/modules/web-ui/public/assets/accessories/glassesRetro.glb differ diff --git a/modules/web-ui/public/assets/accessories/glassesRound.glb b/modules/web-ui/public/assets/accessories/glassesRound.glb new file mode 100644 index 0000000..2f9c66b Binary files /dev/null and b/modules/web-ui/public/assets/accessories/glassesRound.glb differ diff --git a/modules/web-ui/public/assets/accessories/hairBobcut.glb b/modules/web-ui/public/assets/accessories/hairBobcut.glb new file mode 100644 index 0000000..6d91860 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/hairBobcut.glb differ diff --git a/modules/web-ui/public/assets/accessories/hairPigtail.glb b/modules/web-ui/public/assets/accessories/hairPigtail.glb new file mode 100644 index 0000000..bba5d46 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/hairPigtail.glb differ diff --git a/modules/web-ui/public/assets/accessories/hairPonytail.glb b/modules/web-ui/public/assets/accessories/hairPonytail.glb new file mode 100644 index 0000000..d8e39ac Binary files /dev/null and b/modules/web-ui/public/assets/accessories/hairPonytail.glb differ diff --git a/modules/web-ui/public/assets/accessories/hairTail.glb b/modules/web-ui/public/assets/accessories/hairTail.glb new file mode 100644 index 0000000..3801ee8 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/hairTail.glb differ diff --git a/modules/web-ui/public/assets/accessories/militaryBackpack.glb b/modules/web-ui/public/assets/accessories/militaryBackpack.glb new file mode 100644 index 0000000..88761d6 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/militaryBackpack.glb differ diff --git a/modules/web-ui/public/assets/accessories/militaryBeret.glb b/modules/web-ui/public/assets/accessories/militaryBeret.glb new file mode 100644 index 0000000..b90bd45 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/militaryBeret.glb differ diff --git a/modules/web-ui/public/assets/accessories/modernBackpack.glb b/modules/web-ui/public/assets/accessories/modernBackpack.glb new file mode 100644 index 0000000..bcf61a6 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/modernBackpack.glb differ diff --git a/modules/web-ui/public/assets/accessories/racingHelmet.glb b/modules/web-ui/public/assets/accessories/racingHelmet.glb new file mode 100644 index 0000000..00c50cb Binary files /dev/null and b/modules/web-ui/public/assets/accessories/racingHelmet.glb differ diff --git a/modules/web-ui/public/assets/accessories/strawHat.glb b/modules/web-ui/public/assets/accessories/strawHat.glb new file mode 100644 index 0000000..29116f7 Binary files /dev/null and b/modules/web-ui/public/assets/accessories/strawHat.glb differ diff --git a/modules/web-ui/public/assets/assets.json b/modules/web-ui/public/assets/assets.json new file mode 100644 index 0000000..865bf76 --- /dev/null +++ b/modules/web-ui/public/assets/assets.json @@ -0,0 +1,169 @@ +{ + "skins":[ + "alienA","alienB","animalA","animalB","animalBaseA","animalBaseB","animalBaseC","animalBaseD","animalBaseE","animalBaseF" + ,"animalBaseG","animalBaseH","animalBaseI","animalBaseJ","animalC","animalD","animalE","animalF","animalG","animalH","animalI" + ,"animalJ","astroFemaleA","astroFemaleB","astroMaleA","astroMaleB" + ,"athleteFemaleBlue","athleteFemaleGreen","athleteFemaleRed","athleteFemaleYellow","athleteMaleBlue","athleteMaleGreen" + ,"athleteMaleRed","athleteMaleYellow" + ,"businessMaleA","businessMaleB" + ,"casualFemaleA","casualFemaleB","casualMaleA","casualMaleB","cyborg" + ,"fantasyFemaleA","fantasyFemaleB","fantasyMaleA","fantasyMaleB","farmerA","farmerB" + ,"militaryFemaleA","militaryFemaleB","militaryMaleA","militaryMaleB" + ,"racerBlueFemale","racerBlueMale","racerGreenFemale","racerGreenMale","racerOrangeFemale","racerOrangeMale" + ,"racerPurpleFemale","racerPurpleMale","racerRedFemale","racerRedMale","robot","robot2","robot3" + ,"survivorFemaleA","survivorFemaleB","survivorMaleA","survivorMaleB","zombieA","zombieB","zombieC" + ], + "animations": { + "attitudes": ["CrouchIdle","CrouchWalk","Idle","Jump","RacingIdle","Run","Walk"] + ,"actions": ["Attack","Crouch","CrouchIdle","CrouchWalk","Death","Idle","Interact_ground","Interact_standing","Jump","Kick","Punch","RacingIdle","SteerLeft","SteerRight","Run","Shoot","Walk","Wave"] + }, + "accessories": + { + "astroBackpack": { + "position": { + "x": 0, + "y": 0.2, + "z": -0.1 + }, + "bone": "Chest", + "scene": null + }, + "astroHelmet": { + "position": { + "x": 0, + "y": 0.55, + "z": 0 + }, + "bone": "Head", + "scene": null + }, + "beard": { + "position": { + "x": 0, + "y": 0.25, + "z": 0.3 + }, + "bone": "Head", + "scene": null + }, + "cap": { + "position": { + "x": 0, + "y": 0.55, + "z": 0.03 + }, + "bone": "Head", + "scene": null + }, + "farmerCap": { + "position": { + "x": 0, + "y": 0.7, + "z": 0 + }, + "bone": "Head", + "scene": null + }, + "glassesRetro": { + "position": { + "x": 0, + "y": 0.45, + "z": 0.5 + }, + "bone": "Head", + "scene": null + }, + "glassesRound": { + "position": { + "x": 0, + "y": 0.45, + "z": 0.5 + }, + "bone": "Head", + "scene": null + }, + "hairBobcut": { + "position": { + "x": 0, + "y": 0.75, + "z": -0.035 + }, + "bone": "Head", + "scene": null + }, + "hairPigtail": { + "position": { + "x": 0, + "y": 0.8, + "z": -0.4 + }, + "bone": "Head", + "scene": null + }, + "hairPonytail": { + "position": { + "x": 0, + "y": 0.45, + "z": -0.5 + }, + "bone": "Head", + "scene": null + }, + "hairTail": { + "position": { + "x": 0, + "y": 0.4, + "z": -0.4 + }, + "bone": "Head", + "scene": null + }, + "militaryBackpack": { + "position": { + "x": 0, + "y": 0.2, + "z": -0.1 + }, + "bone": "Chest", + "scene": null + }, + "militaryBeret": { + "position": { + "x": -0.05, + "y": 0.95, + "z": 0.035 + }, + "bone": "Head", + "scene": null + }, + "modernBackpack": { + "position": { + "x": 0, + "y": 0.2, + "z": -0.1 + }, + "bone": "Chest", + "scene": null + }, + "racingHelmet": { + "position": { + "x": 0, + "y": 0.4, + "z": 0 + }, + "bone": "Head", + "scene": null + }, + "strawHat": { + "position": { + "x": 0, + "y": 0.7, + "z": -0.05 + }, + "bone": "Head", + "scene": null + } + } + + +} \ No newline at end of file diff --git a/modules/web-ui/public/assets/characterMediumAllAnimations.glb b/modules/web-ui/public/assets/characterMediumAllAnimations.glb new file mode 100644 index 0000000..66fd4e9 Binary files /dev/null and b/modules/web-ui/public/assets/characterMediumAllAnimations.glb differ diff --git a/modules/web-ui/public/assets/skins/alienA.png b/modules/web-ui/public/assets/skins/alienA.png new file mode 100644 index 0000000..fc5be02 Binary files /dev/null and b/modules/web-ui/public/assets/skins/alienA.png differ diff --git a/modules/web-ui/public/assets/skins/alienB.png b/modules/web-ui/public/assets/skins/alienB.png new file mode 100644 index 0000000..0fcaa84 Binary files /dev/null and b/modules/web-ui/public/assets/skins/alienB.png differ diff --git a/modules/web-ui/public/assets/skins/animalA.png b/modules/web-ui/public/assets/skins/animalA.png new file mode 100644 index 0000000..6203611 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalA.png differ diff --git a/modules/web-ui/public/assets/skins/animalB.png b/modules/web-ui/public/assets/skins/animalB.png new file mode 100644 index 0000000..ec47ade Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalB.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseA.png b/modules/web-ui/public/assets/skins/animalBaseA.png new file mode 100644 index 0000000..877fac6 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseA.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseB.png b/modules/web-ui/public/assets/skins/animalBaseB.png new file mode 100644 index 0000000..4262f23 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseB.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseC.png b/modules/web-ui/public/assets/skins/animalBaseC.png new file mode 100644 index 0000000..ebeb69f Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseC.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseD.png b/modules/web-ui/public/assets/skins/animalBaseD.png new file mode 100644 index 0000000..ed5f033 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseD.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseE.png b/modules/web-ui/public/assets/skins/animalBaseE.png new file mode 100644 index 0000000..485d5ae Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseE.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseF.png b/modules/web-ui/public/assets/skins/animalBaseF.png new file mode 100644 index 0000000..9a6e50c Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseF.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseG.png b/modules/web-ui/public/assets/skins/animalBaseG.png new file mode 100644 index 0000000..015f4d6 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseG.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseH.png b/modules/web-ui/public/assets/skins/animalBaseH.png new file mode 100644 index 0000000..04b86cd Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseH.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseI.png b/modules/web-ui/public/assets/skins/animalBaseI.png new file mode 100644 index 0000000..c2227a2 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseI.png differ diff --git a/modules/web-ui/public/assets/skins/animalBaseJ.png b/modules/web-ui/public/assets/skins/animalBaseJ.png new file mode 100644 index 0000000..af4d484 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalBaseJ.png differ diff --git a/modules/web-ui/public/assets/skins/animalC.png b/modules/web-ui/public/assets/skins/animalC.png new file mode 100644 index 0000000..bb10bff Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalC.png differ diff --git a/modules/web-ui/public/assets/skins/animalD.png b/modules/web-ui/public/assets/skins/animalD.png new file mode 100644 index 0000000..d3cac16 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalD.png differ diff --git a/modules/web-ui/public/assets/skins/animalE.png b/modules/web-ui/public/assets/skins/animalE.png new file mode 100644 index 0000000..1bd3372 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalE.png differ diff --git a/modules/web-ui/public/assets/skins/animalF.png b/modules/web-ui/public/assets/skins/animalF.png new file mode 100644 index 0000000..23d921c Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalF.png differ diff --git a/modules/web-ui/public/assets/skins/animalG.png b/modules/web-ui/public/assets/skins/animalG.png new file mode 100644 index 0000000..a05cab7 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalG.png differ diff --git a/modules/web-ui/public/assets/skins/animalH.png b/modules/web-ui/public/assets/skins/animalH.png new file mode 100644 index 0000000..d0c5217 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalH.png differ diff --git a/modules/web-ui/public/assets/skins/animalI.png b/modules/web-ui/public/assets/skins/animalI.png new file mode 100644 index 0000000..26b7b06 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalI.png differ diff --git a/modules/web-ui/public/assets/skins/animalJ.png b/modules/web-ui/public/assets/skins/animalJ.png new file mode 100644 index 0000000..7fbda20 Binary files /dev/null and b/modules/web-ui/public/assets/skins/animalJ.png differ diff --git a/modules/web-ui/public/assets/skins/astroFemaleA.png b/modules/web-ui/public/assets/skins/astroFemaleA.png new file mode 100644 index 0000000..ae77077 Binary files /dev/null and b/modules/web-ui/public/assets/skins/astroFemaleA.png differ diff --git a/modules/web-ui/public/assets/skins/astroFemaleB.png b/modules/web-ui/public/assets/skins/astroFemaleB.png new file mode 100644 index 0000000..0ec111b Binary files /dev/null and b/modules/web-ui/public/assets/skins/astroFemaleB.png differ diff --git a/modules/web-ui/public/assets/skins/astroMaleA.png b/modules/web-ui/public/assets/skins/astroMaleA.png new file mode 100644 index 0000000..c18bd81 Binary files /dev/null and b/modules/web-ui/public/assets/skins/astroMaleA.png differ diff --git a/modules/web-ui/public/assets/skins/astroMaleB.png b/modules/web-ui/public/assets/skins/astroMaleB.png new file mode 100644 index 0000000..7de83bb Binary files /dev/null and b/modules/web-ui/public/assets/skins/astroMaleB.png differ diff --git a/modules/web-ui/public/assets/skins/athleteFemaleBlue.png b/modules/web-ui/public/assets/skins/athleteFemaleBlue.png new file mode 100644 index 0000000..f17cbe1 Binary files /dev/null and b/modules/web-ui/public/assets/skins/athleteFemaleBlue.png differ diff --git a/modules/web-ui/public/assets/skins/athleteFemaleGreen.png b/modules/web-ui/public/assets/skins/athleteFemaleGreen.png new file mode 100644 index 0000000..4e615d5 Binary files /dev/null and b/modules/web-ui/public/assets/skins/athleteFemaleGreen.png differ diff --git a/modules/web-ui/public/assets/skins/athleteFemaleRed.png b/modules/web-ui/public/assets/skins/athleteFemaleRed.png new file mode 100644 index 0000000..fb1ee9e Binary files /dev/null and b/modules/web-ui/public/assets/skins/athleteFemaleRed.png differ diff --git a/modules/web-ui/public/assets/skins/athleteFemaleYellow.png b/modules/web-ui/public/assets/skins/athleteFemaleYellow.png new file mode 100644 index 0000000..632d390 Binary files /dev/null and b/modules/web-ui/public/assets/skins/athleteFemaleYellow.png differ diff --git a/modules/web-ui/public/assets/skins/athleteMaleBlue.png b/modules/web-ui/public/assets/skins/athleteMaleBlue.png new file mode 100644 index 0000000..5f3bf6c Binary files /dev/null and b/modules/web-ui/public/assets/skins/athleteMaleBlue.png differ diff --git a/modules/web-ui/public/assets/skins/athleteMaleGreen.png b/modules/web-ui/public/assets/skins/athleteMaleGreen.png new file mode 100644 index 0000000..5e85728 Binary files /dev/null and b/modules/web-ui/public/assets/skins/athleteMaleGreen.png differ diff --git a/modules/web-ui/public/assets/skins/athleteMaleRed.png b/modules/web-ui/public/assets/skins/athleteMaleRed.png new file mode 100644 index 0000000..672e50a Binary files /dev/null and b/modules/web-ui/public/assets/skins/athleteMaleRed.png differ diff --git a/modules/web-ui/public/assets/skins/athleteMaleYellow.png b/modules/web-ui/public/assets/skins/athleteMaleYellow.png new file mode 100644 index 0000000..c620c0a Binary files /dev/null and b/modules/web-ui/public/assets/skins/athleteMaleYellow.png differ diff --git a/modules/web-ui/public/assets/skins/businessMaleA.png b/modules/web-ui/public/assets/skins/businessMaleA.png new file mode 100644 index 0000000..416b38b Binary files /dev/null and b/modules/web-ui/public/assets/skins/businessMaleA.png differ diff --git a/modules/web-ui/public/assets/skins/businessMaleB.png b/modules/web-ui/public/assets/skins/businessMaleB.png new file mode 100644 index 0000000..c65b91f Binary files /dev/null and b/modules/web-ui/public/assets/skins/businessMaleB.png differ diff --git a/modules/web-ui/public/assets/skins/casualFemaleA.png b/modules/web-ui/public/assets/skins/casualFemaleA.png new file mode 100644 index 0000000..9953294 Binary files /dev/null and b/modules/web-ui/public/assets/skins/casualFemaleA.png differ diff --git a/modules/web-ui/public/assets/skins/casualFemaleB.png b/modules/web-ui/public/assets/skins/casualFemaleB.png new file mode 100644 index 0000000..a1fa847 Binary files /dev/null and b/modules/web-ui/public/assets/skins/casualFemaleB.png differ diff --git a/modules/web-ui/public/assets/skins/casualMaleA.png b/modules/web-ui/public/assets/skins/casualMaleA.png new file mode 100644 index 0000000..61c9d66 Binary files /dev/null and b/modules/web-ui/public/assets/skins/casualMaleA.png differ diff --git a/modules/web-ui/public/assets/skins/casualMaleB.png b/modules/web-ui/public/assets/skins/casualMaleB.png new file mode 100644 index 0000000..e575f06 Binary files /dev/null and b/modules/web-ui/public/assets/skins/casualMaleB.png differ diff --git a/modules/web-ui/public/assets/skins/cyborg.png b/modules/web-ui/public/assets/skins/cyborg.png new file mode 100644 index 0000000..ac7ac0e Binary files /dev/null and b/modules/web-ui/public/assets/skins/cyborg.png differ diff --git a/modules/web-ui/public/assets/skins/fantasyFemaleA.png b/modules/web-ui/public/assets/skins/fantasyFemaleA.png new file mode 100644 index 0000000..475aa96 Binary files /dev/null and b/modules/web-ui/public/assets/skins/fantasyFemaleA.png differ diff --git a/modules/web-ui/public/assets/skins/fantasyFemaleB.png b/modules/web-ui/public/assets/skins/fantasyFemaleB.png new file mode 100644 index 0000000..51bec79 Binary files /dev/null and b/modules/web-ui/public/assets/skins/fantasyFemaleB.png differ diff --git a/modules/web-ui/public/assets/skins/fantasyMaleA.png b/modules/web-ui/public/assets/skins/fantasyMaleA.png new file mode 100644 index 0000000..753bf48 Binary files /dev/null and b/modules/web-ui/public/assets/skins/fantasyMaleA.png differ diff --git a/modules/web-ui/public/assets/skins/fantasyMaleB.png b/modules/web-ui/public/assets/skins/fantasyMaleB.png new file mode 100644 index 0000000..86771b7 Binary files /dev/null and b/modules/web-ui/public/assets/skins/fantasyMaleB.png differ diff --git a/modules/web-ui/public/assets/skins/farmerA.png b/modules/web-ui/public/assets/skins/farmerA.png new file mode 100644 index 0000000..e846aed Binary files /dev/null and b/modules/web-ui/public/assets/skins/farmerA.png differ diff --git a/modules/web-ui/public/assets/skins/farmerB.png b/modules/web-ui/public/assets/skins/farmerB.png new file mode 100644 index 0000000..4488dd0 Binary files /dev/null and b/modules/web-ui/public/assets/skins/farmerB.png differ diff --git a/modules/web-ui/public/assets/skins/militaryFemaleA.png b/modules/web-ui/public/assets/skins/militaryFemaleA.png new file mode 100644 index 0000000..9b503ee Binary files /dev/null and b/modules/web-ui/public/assets/skins/militaryFemaleA.png differ diff --git a/modules/web-ui/public/assets/skins/militaryFemaleB.png b/modules/web-ui/public/assets/skins/militaryFemaleB.png new file mode 100644 index 0000000..d15551e Binary files /dev/null and b/modules/web-ui/public/assets/skins/militaryFemaleB.png differ diff --git a/modules/web-ui/public/assets/skins/militaryMaleA.png b/modules/web-ui/public/assets/skins/militaryMaleA.png new file mode 100644 index 0000000..4bfe47c Binary files /dev/null and b/modules/web-ui/public/assets/skins/militaryMaleA.png differ diff --git a/modules/web-ui/public/assets/skins/militaryMaleB.png b/modules/web-ui/public/assets/skins/militaryMaleB.png new file mode 100644 index 0000000..5c65009 Binary files /dev/null and b/modules/web-ui/public/assets/skins/militaryMaleB.png differ diff --git a/modules/web-ui/public/assets/skins/racerBlueFemale.png b/modules/web-ui/public/assets/skins/racerBlueFemale.png new file mode 100644 index 0000000..fdc3383 Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerBlueFemale.png differ diff --git a/modules/web-ui/public/assets/skins/racerBlueMale.png b/modules/web-ui/public/assets/skins/racerBlueMale.png new file mode 100644 index 0000000..cee4a06 Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerBlueMale.png differ diff --git a/modules/web-ui/public/assets/skins/racerGreenFemale.png b/modules/web-ui/public/assets/skins/racerGreenFemale.png new file mode 100644 index 0000000..d926007 Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerGreenFemale.png differ diff --git a/modules/web-ui/public/assets/skins/racerGreenMale.png b/modules/web-ui/public/assets/skins/racerGreenMale.png new file mode 100644 index 0000000..74e562d Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerGreenMale.png differ diff --git a/modules/web-ui/public/assets/skins/racerOrangeFemale.png b/modules/web-ui/public/assets/skins/racerOrangeFemale.png new file mode 100644 index 0000000..de59279 Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerOrangeFemale.png differ diff --git a/modules/web-ui/public/assets/skins/racerOrangeMale.png b/modules/web-ui/public/assets/skins/racerOrangeMale.png new file mode 100644 index 0000000..d30693a Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerOrangeMale.png differ diff --git a/modules/web-ui/public/assets/skins/racerPurpleFemale.png b/modules/web-ui/public/assets/skins/racerPurpleFemale.png new file mode 100644 index 0000000..36c7d5b Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerPurpleFemale.png differ diff --git a/modules/web-ui/public/assets/skins/racerPurpleMale.png b/modules/web-ui/public/assets/skins/racerPurpleMale.png new file mode 100644 index 0000000..50f92d5 Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerPurpleMale.png differ diff --git a/modules/web-ui/public/assets/skins/racerRedFemale.png b/modules/web-ui/public/assets/skins/racerRedFemale.png new file mode 100644 index 0000000..6171561 Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerRedFemale.png differ diff --git a/modules/web-ui/public/assets/skins/racerRedMale.png b/modules/web-ui/public/assets/skins/racerRedMale.png new file mode 100644 index 0000000..07d9201 Binary files /dev/null and b/modules/web-ui/public/assets/skins/racerRedMale.png differ diff --git a/modules/web-ui/public/assets/skins/robot.png b/modules/web-ui/public/assets/skins/robot.png new file mode 100644 index 0000000..ae1c092 Binary files /dev/null and b/modules/web-ui/public/assets/skins/robot.png differ diff --git a/modules/web-ui/public/assets/skins/robot2.png b/modules/web-ui/public/assets/skins/robot2.png new file mode 100644 index 0000000..0d32abf Binary files /dev/null and b/modules/web-ui/public/assets/skins/robot2.png differ diff --git a/modules/web-ui/public/assets/skins/robot3.png b/modules/web-ui/public/assets/skins/robot3.png new file mode 100644 index 0000000..f7a46d2 Binary files /dev/null and b/modules/web-ui/public/assets/skins/robot3.png differ diff --git a/modules/web-ui/public/assets/skins/survivorFemaleA.png b/modules/web-ui/public/assets/skins/survivorFemaleA.png new file mode 100644 index 0000000..94c35ef Binary files /dev/null and b/modules/web-ui/public/assets/skins/survivorFemaleA.png differ diff --git a/modules/web-ui/public/assets/skins/survivorFemaleB.png b/modules/web-ui/public/assets/skins/survivorFemaleB.png new file mode 100644 index 0000000..721d792 Binary files /dev/null and b/modules/web-ui/public/assets/skins/survivorFemaleB.png differ diff --git a/modules/web-ui/public/assets/skins/survivorMaleA.png b/modules/web-ui/public/assets/skins/survivorMaleA.png new file mode 100644 index 0000000..0e39d42 Binary files /dev/null and b/modules/web-ui/public/assets/skins/survivorMaleA.png differ diff --git a/modules/web-ui/public/assets/skins/survivorMaleB.png b/modules/web-ui/public/assets/skins/survivorMaleB.png new file mode 100644 index 0000000..cd783cb Binary files /dev/null and b/modules/web-ui/public/assets/skins/survivorMaleB.png differ diff --git a/modules/web-ui/public/assets/skins/zombieA.png b/modules/web-ui/public/assets/skins/zombieA.png new file mode 100644 index 0000000..d64f671 Binary files /dev/null and b/modules/web-ui/public/assets/skins/zombieA.png differ diff --git a/modules/web-ui/public/assets/skins/zombieB.png b/modules/web-ui/public/assets/skins/zombieB.png new file mode 100644 index 0000000..d129763 Binary files /dev/null and b/modules/web-ui/public/assets/skins/zombieB.png differ diff --git a/modules/web-ui/public/assets/skins/zombieC.png b/modules/web-ui/public/assets/skins/zombieC.png new file mode 100644 index 0000000..c0da4b6 Binary files /dev/null and b/modules/web-ui/public/assets/skins/zombieC.png differ diff --git a/modules/web-ui/public/conf/config.json b/modules/web-ui/public/conf/config.json new file mode 100644 index 0000000..d9fe961 --- /dev/null +++ b/modules/web-ui/public/conf/config.json @@ -0,0 +1,4 @@ +{ + "MQTT_URI": "ws://localhost:9001/mqtt", + "MQTT_TOPIC_ROOT": "microsquad" +} diff --git a/modules/web-ui/public/index.html b/modules/web-ui/public/index.html new file mode 100644 index 0000000..7e01aac --- /dev/null +++ b/modules/web-ui/public/index.html @@ -0,0 +1,47 @@ + + +Microsquad + + + + MicroSquad + + + +
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+

+

+
+
+ + \ No newline at end of file diff --git a/modules/web-ui/sonar-project.properties b/modules/web-ui/sonar-project.properties new file mode 100644 index 0000000..50b9bb9 --- /dev/null +++ b/modules/web-ui/sonar-project.properties @@ -0,0 +1,6 @@ +sonar.organization=lucasvanmol +sonar.projectKey=lucasvanmol_usquad-web-ui + +# relative paths to source directories. More details and properties are described +# in https://sonarcloud.io/documentation/project-administration/narrowing-the-focus/ +sonar.sources=. diff --git a/modules/web-ui/source-path.sh b/modules/web-ui/source-path.sh new file mode 100644 index 0000000..8257f49 --- /dev/null +++ b/modules/web-ui/source-path.sh @@ -0,0 +1 @@ +export PATH=`pwd`/node:`pwd`/src:$PATH diff --git a/modules/web-ui/src/accessories.json b/modules/web-ui/src/accessories.json new file mode 100644 index 0000000..5952fa7 --- /dev/null +++ b/modules/web-ui/src/accessories.json @@ -0,0 +1,146 @@ +{ + "astroBackpack": { + "position": { + "x": 0, + "y": 0.2, + "z": -0.1 + }, + "bone": "Chest", + "scene": null + }, + "astroHelmet": { + "position": { + "x": 0, + "y": 0.55, + "z": 0 + }, + "bone": "Head", + "scene": null + }, + "beard": { + "position": { + "x": 0, + "y": 0.25, + "z": 0.3 + }, + "bone": "Head", + "scene": null + }, + "cap": { + "position": { + "x": 0, + "y": 0.55, + "z": 0.03 + }, + "bone": "Head", + "scene": null + }, + "farmerCap": { + "position": { + "x": 0, + "y": 0.7, + "z": 0 + }, + "bone": "Head", + "scene": null + }, + "glassesRetro": { + "position": { + "x": 0, + "y": 0.45, + "z": 0.5 + }, + "bone": "Head", + "scene": null + }, + "glassesRound": { + "position": { + "x": 0, + "y": 0.45, + "z": 0.5 + }, + "bone": "Head", + "scene": null + }, + "hairBobcut": { + "position": { + "x": 0, + "y": 0.75, + "z": -0.035 + }, + "bone": "Head", + "scene": null + }, + "hairPigtail": { + "position": { + "x": 0, + "y": 0.8, + "z": -0.4 + }, + "bone": "Head", + "scene": null + }, + "hairPonytail": { + "position": { + "x": 0, + "y": 0.45, + "z": -0.5 + }, + "bone": "Head", + "scene": null + }, + "hairTail": { + "position": { + "x": 0, + "y": 0.4, + "z": -0.4 + }, + "bone": "Head", + "scene": null + }, + "militaryBackpack": { + "position": { + "x": 0, + "y": 0.2, + "z": -0.1 + }, + "bone": "Chest", + "scene": null + }, + "militaryBeret": { + "position": { + "x": -0.05, + "y": 0.95, + "z": 0.035 + }, + "bone": "Head", + "scene": null + }, + "modernBackpack": { + "position": { + "x": 0, + "y": 0.2, + "z": -0.1 + }, + "bone": "Chest", + "scene": null + }, + "racingHelmet": { + "position": { + "x": 0, + "y": 0.4, + "z": 0 + }, + "bone": "Head", + "scene": null + }, + "strawHat": { + "position": { + "x": 0, + "y": 0.7, + "z": -0.05 + }, + "bone": "Head", + "scene": null + } +} \ No newline at end of file diff --git a/modules/web-ui/src/app.ts b/modules/web-ui/src/app.ts new file mode 100644 index 0000000..d213c4d --- /dev/null +++ b/modules/web-ui/src/app.ts @@ -0,0 +1,513 @@ +import * as THREE from "three"; +import { Subject } from 'rxjs'; +import { MQTTClient, MqttMicrosquadEventType,MqttUpdateEvent } from "./mqtt"; +import { PlayerManager } from './playerManager'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; +import { Context, UpdateObject } from "./updateObject"; +import envConfig from './config'; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { Player } from "./player"; +import { Scoreboard } from "./scoreboard"; +import 'bootstrap'; +import 'bootstrap/dist/css/bootstrap.min.css'; + +var config = envConfig; + +var assetsConfig : any; + +var mqttTopicRoot : string; + +var mqttClient :MQTTClient; + +var mqttClientId : string; + +const playerSubject : Subject = new Subject(); +const teamSubject : Subject = new Subject(); +const scoreboardSubject : Subject = new Subject(); + +// var sessionCode = "session-default"; + +var gameName = ""; + +var mqttSubscriptionRoot:string; + +var playerStates = new Map(); +var teamStates = new Map(); + +const loader = new THREE.FileLoader(); + + +function startMqttSubscriptions(){ + const queryString = window.location.search; + const urlParams = new URLSearchParams(window.location.search); + // sessionCode = urlParams.get('sc') ?? "session-default"; + gameName = urlParams.get('gn') ?? ""; + const urlClientId = urlParams.get('ci'); + if (urlClientId != null) { + mqttClientId = "microsquad-web:" + urlClientId; // if specified in the URL, retain the same client ID + } + else { + mqttClientId = "microsquad-web:" + Math.random().toString(36).substr(2, 5); // unique client ID to prevent reconnect loop + } + + mqttClient = new MQTTClient( + config.MQTT_URI, + mqttClientId, + onMessageArrived, + onMqttConnect, + onMqttConnectionLost, + ); +} + +var assetsInitialized:boolean = false; + +loader.load('assets/assets.json', + function ( data ) { + assetsConfig = JSON.parse(data); + + //load a text file and output the result to the console + loader.load( + // resource URL + 'conf/config.json', + + // onLoad callback + function ( data ) { + config = JSON.parse(data); + initializeAssetsSettings(); + startMqttSubscriptions(); + + }, + undefined, + // onError callback + function ( err ) { + console.error( 'Could not load JSON configuration at conf/config.json - using Node env configuration' ); + initializeAssetsSettings(); + startMqttSubscriptions(); + } + ); + + }, + undefined, + // onError callback + function ( err ) { + console.error( 'Could not load assets JSON configuration at assets/assets.json' ); + } +) + + + + +/////////////////////////////////////////// SCENE SETUP //////////////////////////////////////////// + +function fitCameraToObject( cam : THREE.PerspectiveCamera, object : THREE.Object3D, offset?: number, cntrls? : OrbitControls ) { + + offset = offset || 1.1; + const boundingBox = new THREE.Box3(); + + // get bounding box of object - this will be used to setup controls and camera + boundingBox.setFromObject( object ); + const center = new THREE.Vector3() + boundingBox.getCenter(center); + const size = boundingBox.getSize(center); + + // get the max side of the bounding box (fits to width OR height as needed ) + const maxDim = Math.max( size.x, size.y, size.z ); + const fov = cam.fov * ( Math.PI / 180 ); + let camZ = Math.abs( maxDim / 4 * Math.tan( fov * 2 ) ); + + camZ *= offset; // zoom out a little so that objects don't fill the screen + cam.position.z = camZ; + + const minZ = boundingBox.min.z; + const cameraToFarEdge = ( minZ < 0 ) ? -minZ + camZ : camZ - minZ; + + cam.far = cameraToFarEdge * 3; + cam.updateProjectionMatrix(); + + if ( cntrls ) { + // set camera to rotate around center of loaded object + cntrls.target = center; + // prevent camera from zooming out far enough to create far plane cutoff + cntrls.maxDistance = cameraToFarEdge * 2; + cntrls.saveState(); + } else { + cam.lookAt( center ) + } +} + +const renderer = new THREE.WebGLRenderer( {antialias: true } ); +renderer.setPixelRatio( window.devicePixelRatio ); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.shadowMap.enabled = true; +renderer.shadowMap.type = THREE.PCFSoftShadowMap; +renderer.outputEncoding = THREE.sRGBEncoding; +document.body.appendChild(renderer.domElement); + +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x418afb); +const ambientColor = 0xFFFFC5; +const ambiIntensity = 0.7; +const ambilight = new THREE.AmbientLight(ambientColor, ambiIntensity); + +const geo = new THREE.CircleGeometry(20, 20, 32); +const mat = new THREE.MeshStandardMaterial({ color: 0xe4ca4c, side: THREE.DoubleSide }); +var plane = new THREE.Mesh(geo, mat); +plane.receiveShadow = true; +plane.rotateX( - Math.PI / 2); +scene.add(plane); + +const dirColor = 0xffffaa; +const dirIntensity = 0.7; +const dirlight = new THREE.DirectionalLight(dirColor, dirIntensity); +dirlight.position.set(0,2,0); +dirlight.castShadow = true; +const helper = new THREE.DirectionalLightHelper(dirlight); + +const clock = new THREE.Clock(); +var objects: UpdateObject[] = []; + + +const camera = new THREE.PerspectiveCamera( + 45, // FOV + window.innerWidth / window.innerHeight, // Ratio + 0.1, 1000 // Near / Far Clip +); +camera.position.set(0, 0, -6); +// camera.zoom = 20; + +const controls = new OrbitControls( camera, renderer.domElement ); +controls.enableDamping = false; +// controls.dampingFactor = 0.1; +controls.enablePan = false; +controls.target.set(0, 4, 1); +controls.minPolarAngle = controls.getPolarAngle(); +controls.maxPolarAngle = controls.getPolarAngle(); +controls.maxAzimuthAngle = controls.getAzimuthalAngle(); +controls.minAzimuthAngle = controls.getAzimuthalAngle(); +// let dist = camera.position.distanceTo(controls.target); +controls.maxDistance = 10; +camera.updateMatrixWorld(); + +var context : Context = { + scene: scene, + camera: camera, + renderer: renderer, + objList: objects, +}; +UpdateObject.context = context; + +var playerManager = new PlayerManager(playerSubject); + +window['playerManager'] = playerManager; + +// var addPlayerButton : HTMLButtonElement = document.getElementById("add-player"); +// addPlayerButton.addEventListener('click', () => { playerManager.addPlayer("Player:"+ Math.random().toString(36).substr(2, 5), true) }); + + +// var zoomScreenButton : HTMLButtonElement = document.getElementById("zoom-screen"); +// zoomScreenButton.addEventListener('click', () => { fitCameraToObject(camera,scoreboard.mesh) }); + +var scoreboard = new Scoreboard(UpdateObject.context, scoreboardSubject); + +window['scoreboard'] = scoreboard; +////////////////////////////////////////// ASSET LOADING /////////////////////////////////////////// + +const manager = new THREE.LoadingManager(); + +manager.onStart = () => { + console.log("Load start..."); +} + +manager.onProgress = ( url, itemsLoaded, itemsTotal ) => { + console.log(`Loading (${itemsLoaded}/${itemsTotal}): ${url}`); +} + +manager.onError = (url) => { + console.log(`Error loading: ${url}`); +}; + +// Animations in gltf.animations that need to be looped +interface AnimationInfo { + animation : THREE.AnimationClip, + loop : boolean, +} + +const gltfLoader = new GLTFLoader(manager); + +const asset_url = "assets/characterMediumAllAnimations.glb"; +var playerSkins = {}; +var loopedAnimations = []; +var accessories = {}; + +///////////// CHARACTER & ANIMATIONS ///////////// +function initializeAssetsSettings(){ + ///////////////// SKIN TEXTURES ////////////////// + + var texLoader = new THREE.TextureLoader(manager); + + let skin_names = assetsConfig.skins; + + let playerSkins = {}; + skin_names.forEach(skin => { + let map = texLoader.load("assets/skins/" + skin +".png"); + map.encoding = THREE.sRGBEncoding; + map.flipY = false; + playerSkins[skin] = map; + }); + + loopedAnimations = assetsConfig.animations.attitudes; + + gltfLoader.load(asset_url, ( gltf ) => { + + Player.gltf = gltf; + + gltf.scene.traverse( function( node ) { + if ( node.isObject3D ) { node.castShadow = true; } + } ); + + gltf.animations.forEach(anim => { + + let animInfo : AnimationInfo = { + animation : anim, + loop : loopedAnimations.includes(anim.name), + }; + + Player.animations[anim.name] = animInfo; + + }); + + }); + + for (var accessory in assetsConfig.accessories) { + let url = `assets/accessories/${accessory}.glb` + gltfLoader.load(url, (gltf) => { + let filename = url.split("/").pop(); + let accessoryName = filename.split(".")[0]; + accessories[accessoryName] = assetsConfig.accessories[accessoryName] + accessories[accessoryName].scene = gltf.scene; + }); + } + manager.onLoad = () => { + Player.accessories = assetsConfig.accessories; + Player.skins = playerSkins; + } + + setupThreeJsScene(); +} + + + +function setupThreeJsScene(){ + + ///////////////////////////////////////////// LIGHTING ///////////////////////////////////////////// + + // Ambient Light + ambilight.visible = true; + scene.add(ambilight); + + // Directional light + dirlight.position.set(0, 10, 0); + dirlight.target.position.set(2, 4, 6); + scene.add(dirlight); + scene.add(dirlight.target); + dirlight.visible = true; + helper.visible = false; + scene.add(helper); + + ////////////////////////////////////// RENDERING & ANIMATION /////////////////////////////////////// + + window.addEventListener('resize', onWindowResize, false); + function onWindowResize() { + // recalculate camera zoom + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + render(); + } + + var animate = function () { + requestAnimationFrame(animate); + + var delta = clock.getDelta(); + controls.update(); + + objects.forEach(obj => { + obj.update(delta); + }); + + render(); + }; + + function render() { + renderer.render(scene, camera); + } + animate(); +} + +///////////////////////////////////////// COMMAND HANDLING ///////////////////////////////////////// + +function onMessageArrived(message : any) { + commandHandler(message.destinationName, message.payloadString); +} + +// function teamCommandHandler(command: string[], teamID: string) { + +// switch (command[0]) { + +// case _cmdStringChangeSkin: +// case _cmdStringChangeAnimation: +// case _cmdStringSay: +// case _cmdStringChangeAccessory: +// case _cmdStringAssignTeam: +// // Run command for every player in team +// if (!(teamID in playerManager.teams)) { +// console.warn(`Team "${teamID}" does not exist`); +// } else { +// playerManager.teams[teamID].players.forEach( (player) => { +// playerCommandHandler(command, player.id); +// }); +// } +// break; + +// case _cmdStringSplitTeams: +// let teamNames = command.splice(1); +// let i = 0; +// let tot = Object.keys(playerManager.players).length; +// for (let playerName in playerManager.players) { +// playerManager.assignTeam(playerName, teamNames[Math.floor(i/tot * teamNames.length)]); +// i++; +// } +// break; + +// case "reset": +// // If teamID is not specified, reset all teams +// if (teamID) { + +// if (!(teamID in playerManager.teams)) { +// console.warn(`Team "${teamID}" does not exist`); +// } else { +// playerManager.teams[teamID].players.forEach(player => { +// playerManager.assignTeam(player.id, playerManager.defaultTeam.name); +// }); +// } + +// } else { +// for (let playerName in playerManager.players) { +// playerManager.assignTeam(playerName, playerManager.defaultTeam.name); +// } +// } +// break; + +// default: +// break; +// } +// } + + +function commandHandler(incomingTopic, value:string) { + let topic = incomingTopic.substring(mqttSubscriptionRoot.length-1); + let topicParts = topic.split("/"); + + if(topicParts.slice(-1)[0].startsWith("$")){ + // This incoming message is a homie metadata topic, we can ignore it + return; + } + + if (topicParts[0] == "gateway") { + const PLAYER_NODE_PREFIX = "player-"; + const TEAM_NODE_PREFIX = "team-"; + const SCOREBOARD_NODE_PREFIX = "scoreboard"; + const GAME_NODE_PREFIX = "game"; + + const nodeName = topicParts[1]; + ///////////// + // If the message concerns a player or a team, we store its state for later reference + // Eventually, we could keep it in a store implementation - for the time being, maps of maps + if (nodeName.startsWith(PLAYER_NODE_PREFIX) || + nodeName.startsWith(TEAM_NODE_PREFIX) ) { + let devicePrefix : string; + let stateMap : Map; + let propertyName : string; + let eventType : MqttMicrosquadEventType; + let subject: Subject; + if (nodeName.startsWith(PLAYER_NODE_PREFIX)) { + devicePrefix = PLAYER_NODE_PREFIX; + stateMap = playerStates; + eventType = MqttMicrosquadEventType.PLAYER_UPDATE; + subject = playerSubject; + } else if (nodeName.startsWith(TEAM_NODE_PREFIX)) { + devicePrefix = TEAM_NODE_PREFIX; + stateMap = teamStates; + eventType = MqttMicrosquadEventType.TEAM_UPDATE; + subject = teamSubject; + } + if (devicePrefix != null) { + let deviceId = nodeName.substring(devicePrefix.length); + let propertyName = topicParts[2]; + let state = stateMap.get(deviceId) ?? new Map(); + state.set(propertyName, value); + stateMap.set(deviceId, state); + + subject.next(new MqttUpdateEvent(eventType, deviceId, propertyName, value)); + } + } else if (topicParts[1].startsWith(SCOREBOARD_NODE_PREFIX)){ + scoreboardSubject.next(new MqttUpdateEvent(MqttMicrosquadEventType.SCOREBOARD_UPDATE, null, topicParts[2], value)); + } else if (topicParts[1].startsWith(GAME_NODE_PREFIX)){ + // If the list of transitions available has changed, add buttons allowing to trigger them by modifying "fire-transition" + if(topicParts[2] == "transitions"){ + var controlsDiv = document.getElementById("transition-controls"); + controlsDiv.innerHTML=""; + if(value.trim() != ""){ + value.split(",").forEach(transition => { + var transitionButton : HTMLAnchorElement = document.createElement("a"); + transitionButton.classList.add("btn", "btn-primary", "btn-sm"); + transitionButton.setAttribute("role", "button"); + transitionButton.innerHTML = transition; + transitionButton.setAttribute("data-transition-name", transition); + transitionButton.addEventListener('click', event => { + var trns = (event.target as Element).getAttribute("data-transition-name"); + console.log("firing transition "+trns); + fireTransitionViaMQTT(trns) + }); + controlsDiv.appendChild(transitionButton); + }); + } + } + + } + + // + //////////////// + } + +} + +function onMqttConnect() { + console.log("Connected to " + mqttClient.uri); + if(config.MQTT_TOPIC_ROOT != null){ + mqttTopicRoot = config.MQTT_TOPIC_ROOT + } + mqttSubscriptionRoot = mqttTopicRoot +"/#"; + setTimeout(function(){ + mqttClient.subscribe(mqttSubscriptionRoot); + // Update the game name + updateGameNameViaMQTT(); + },500); + // subButton.disabled = false; + // pubButton.disabled = false; + +} + +function updateGameNameViaMQTT(){ + mqttClient.publish(mqttTopicRoot + "/gateway/game/name/set", gameName); +} + +function fireTransitionViaMQTT(transition){ + mqttClient.publish(mqttTopicRoot + "/gateway/game/fire-transition/set", transition); +} + +function onMqttConnectionLost(response) { + if (response.errorCode !== 0) { + console.error("Connection lost: " + response.errorMessage); + } +} diff --git a/modules/web-ui/src/config.ts b/modules/web-ui/src/config.ts new file mode 100644 index 0000000..764f435 --- /dev/null +++ b/modules/web-ui/src/config.ts @@ -0,0 +1,11 @@ +let config = { + MQTT_URI: process.env.MQTT_URI || "ws://broker.emqx.io:8083", + MQTT_TOPIC_ROOT: process.env.MQTT_TOPIC_ROOT || "microsquad", +} + +if (process.env.NODE_ENV === 'production') { + config.MQTT_URI = "MQTT_URI_REPLACE"; + config.MQTT_TOPIC_ROOT = "MQTT_TOPIC_ROOT_REPLACE"; +} + +export default config; \ No newline at end of file diff --git a/modules/web-ui/src/dialogbox3D.ts b/modules/web-ui/src/dialogbox3D.ts new file mode 100644 index 0000000..d782119 --- /dev/null +++ b/modules/web-ui/src/dialogbox3D.ts @@ -0,0 +1,19 @@ +import { Vector3 } from "three"; +import { TextBox3D } from "./textbox3D" + +export class DialogBox3D extends TextBox3D { + timeout: number; + elapsedTime: number = 0; + constructor(text: string, position: Vector3, timeout: number) { + super(text, position, true); + this.timeout = timeout; + } + + update(delta: number) { + super.update(delta); + this.elapsedTime += delta; + if (this.elapsedTime > this.timeout) { + this.destroy(); + } + } +} \ No newline at end of file diff --git a/modules/web-ui/src/mqtt.ts b/modules/web-ui/src/mqtt.ts new file mode 100644 index 0000000..0204029 --- /dev/null +++ b/modules/web-ui/src/mqtt.ts @@ -0,0 +1,77 @@ +import * as MQTT from 'paho-mqtt'; + +export class MQTTClient { + client : MQTT.Client; + uri : string; + + constructor (uri: string, clientID : string, messageArrivedCallback : (message : MQTT.Message) => void, onConnectCallback? : () => void, connectionLostCallback? : (response: any) => void) { + this.uri = uri; + this.client = new MQTT.Client(uri, clientID); + + + // Callback handlers + this.client.onConnectionLost = connectionLostCallback || this._onConnectionLost; + this.client.onMessageArrived = messageArrivedCallback; + + this.client.connect({ + timeout: 10, + onSuccess: onConnectCallback || this._onConnect, + onFailure: this._onFailure, + reconnect: true, + }); + } + + _onConnect() { + console.log("Succesfully Connected"); + } + + _onConnectionLost(responseObject : any) { + if (responseObject.errorCode !== 0) { + console.error("Connection lost: " + responseObject.errorMessage); + } + } + + _onFailure(message : any) { + console.error("Connection failed: " + message); + } + + publish(topic : string, payload : string) { + console.log("Sending message:\nTopic: " + topic +"\nPayload: " + payload); + + let message = new MQTT.Message(payload); + message.destinationName = topic; + + this.client.send(message); + } + + subscribe(topic : string) { + // let subs = document.getElementById('subscriptions'); + // subs.innerHTML += "Subscribed to " + topic + "
"; + + console.log("MQTT : Subscribed to " + topic); + this.client.subscribe(topic); + } +} + +export enum MqttMicrosquadEventType { + PLAYER_UPDATE, + TEAM_UPDATE, + SCOREBOARD_UPDATE, + GAME_UPDATE +} + +export class MqttUpdateEvent { + type: MqttMicrosquadEventType; + id: string; + property: string; + newValue: any; + oldValue: any; + + constructor(type : MqttMicrosquadEventType,id:string, property:string, newValue, oldValue = null ){ + this.type = type; + this.id = id; + this.property = property; + this.newValue = newValue; + this.oldValue = oldValue; + } +} \ No newline at end of file diff --git a/modules/web-ui/src/player.ts b/modules/web-ui/src/player.ts new file mode 100644 index 0000000..5c45036 --- /dev/null +++ b/modules/web-ui/src/player.ts @@ -0,0 +1,244 @@ +import { AnimationMixer, Bone, Group, LoopOnce, Material, Mesh, Object3D, Vector3 } from 'three'; +import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'; +import { TextBox3D } from './textbox3D'; +import { UpdateObject } from './updateObject'; +import { SkeletonUtils } from 'three/examples/jsm/utils/SkeletonUtils' +import { DialogBox3D } from './dialogbox3D'; +import { Team } from './team'; + + +export class Player extends UpdateObject { + static gltf : GLTF; + static animations = {}; + static model_scale = 0.7; + static nametag_vertical_offset = -0.8; + static dialog_height = 4.8; + static accessories; + static skins = {}; + + id : string; + order: string; + _scaleFactor: number = 1.0; + _sayDuration: number = 4000; + + private _nickname : string; + + team : Team; + model : Object3D; + model_loaded : boolean = false; + mixer : AnimationMixer; + nametag : TextBox3D; + dialog_box : DialogBox3D; + private _skin : string; + private _accessory : string; + + constructor (id : string, team: Team, order : string = "00000") { + super(); + this.id = id; + this.team = team; + this.order = order; + team.addPlayer(this); + + this.nametag = new TextBox3D(id, new Vector3(0, 0, 0), false, "20px"); + this.nametag.visible = false; + + if (Player.gltf) { + this.setModel(); + } + } + + private setModel() { + this.model_loaded = true; + this.model = SkeletonUtils.clone(Player.gltf.scene); + this.model.scale.set( Player.model_scale, Player.model_scale, Player.model_scale ); + this.model.castShadow = true; + this.model.receiveShadow = true; + UpdateObject.context.scene.add( this.model ); + + // Set random skin + let skins_names = Object.keys(Player.skins); + let random_skin = skins_names[skins_names.length * Math.random() << 0]; + this.skin = random_skin; + + this.mixer = new AnimationMixer( this.model ); + + this.changeAnimation("Idle"); + } + + changeAnimation(name: string) { + if (name in Player.animations) { + let animInfo = Player.animations[name]; + this.mixer.stopAllAction(); + var action = this.mixer.clipAction(animInfo.animation); + if (!animInfo.loop) { + action.setLoop(LoopOnce, 1); + action.clampWhenFinished = true; + } + action.play(); + } else { + console.warn(`Animation "${name}" not found!`); + } + } + + changeTeam(team: Team) { + this.team.removePlayer(this); + this.team = team; + team.addPlayer(this); + } + + say(message: string) { + if (this.dialog_box) { + this.dialog_box.destroy(); + } + if( ! (message === "")){ + let p = this.position.clone(); + p.y += Player.dialog_height * this.scale; + this.dialog_box = new DialogBox3D(message, p, (this._sayDuration/1000)); + } + } + + set accessory(name: string) { + if (name in Player.accessories) { + let accessory = Player.accessories[name]; + + // Remove old accessory + if (this._accessory) { + this.model.traverse( (object) => { + if (object instanceof Group && object.name === this._accessory) { + object.parent.remove(object); + } + }); + } + + + this.model.traverse( (object) => { + if ( object instanceof Bone && object.name === accessory.bone) { + let sc = accessory.scene.clone(); + sc.name = name; + sc.position.x = accessory.position.x; + sc.position.y = accessory.position.y; + sc.position.z = accessory.position.z; + object.add(sc); + } + }); + this._accessory = name; + + } else { + console.warn(`Accessory "${name}" not found!`) + } + } + + get accessory() { + return this._accessory; + } + + set nickname(newHTMLName: string){ + this._nickname = newHTMLName; + if(! (this._nickname === "") ){ + this.nametag.textElement.innerHTML = this._nickname; + this.nametag.visible = true; + }else{ + this.nametag.visible = false; + } + + } + get nickname(){ + return this._nickname; + } + + get sayDuration(){ + return this._sayDuration; + } + + set sayDuration(sayDuration:number){ + this._sayDuration = sayDuration; + } + + set skin(name: string) { + if (name in Player.skins) { + var mat; // https://discourse.threejs.org/t/giving-a-glb-a-texture-in-code/15071/6 + + this.model.castShadow = true; + this.model.receiveShadow = true; + + this.model.traverse( (object) => { + + if ( object instanceof Mesh ) { + mat = (object.material).clone(); + mat.map = Player.skins[name]; + mat.roughness = 0.85; + mat.needsUpdate = true; + object.material = mat; + } + + }); + this._skin = name; + } else { + console.warn(`Skin "${name}" not found!`); + } + } + + get skin() { + return this._skin; + } + + set rotation(val: Vector3) { + if (this.model) { + this.model.rotation.set(val.x, val.y, val.z); + } + } + + get rotation() { + return this.model.rotation.toVector3(); + } + + set position(val: Vector3) { + if (this.model) { + this.model.position.set(val.x, val.y, val.z); + this.nametag.position.copy(this.model.position).y += Player.nametag_vertical_offset * this.scale; + } + } + + get position() { + return this.model.position; + } + + set scale(val : number) { + if (this.model) { + val *= this._scaleFactor; + this.model.scale.set(val, val, val); + this.nametag.position.copy(this.model.position).y += Player.nametag_vertical_offset * this.scale; + } + } + + get scale() { + return this.model.scale.x; + } + + set scaleFactor(val : number){ + this._scaleFactor = val; + } + + get scaleFactor(){ + return this._scaleFactor; + } + + update(delta : number) { + if (this.mixer) { this.mixer.update( delta ) } + if (!this.model_loaded && Player.gltf) { this.setModel() } + } + + destroy() { + this.team.removePlayer(this); + + UpdateObject.context.scene.remove( this.model ); + + this.model.traverse((object) => { + let obj = object; + if (obj.geometry !== undefined) { + obj.geometry.dispose(); + obj.material.dispose(); + } + }); + } +} \ No newline at end of file diff --git a/modules/web-ui/src/playerManager.ts b/modules/web-ui/src/playerManager.ts new file mode 100644 index 0000000..69e07af --- /dev/null +++ b/modules/web-ui/src/playerManager.ts @@ -0,0 +1,152 @@ +import { Player } from "./player"; +import { Vector3 } from "three"; +import { Team } from "./team"; +import { Observable, Observer} from "rxjs"; +import { MqttMicrosquadEventType, MqttUpdateEvent } from "./mqtt"; + +export class PlayerManager { + players: { [name: string]: Player } = {}; + teams: { [name: string]: Team } = {}; + defaultTeam: Team; + + circleRadius: number = 10; // min radius of player circle + circleMaxAngle: number = 2 * Math.PI / 3; // max angle between first and last player + arcDistPlayers: number = 2; // arc distance between adjacent players + arcDistTeams: number = 5; // arc distance between adjacent teams + + observer = { + next: (event) => {this.handleMQTTUpdateEvent(event)}, + error: err => console.log("Error handling MQTT Update Event "+err) + }; + + constructor (observable: Observable) { + this.defaultTeam = new Team("__default__", [], true); + this.teams["__default__"] = this.defaultTeam; + observable.subscribe(this.observer); + } + + handleMQTTUpdateEvent(event : MqttUpdateEvent){ + // console.log("Player Manager : new update "+event.id+" "+event.property); + let playerId = event.id + + if(!this.hasPlayer(playerId)){ + this.addPlayer(playerId, false); + } + + switch (event.property) { + case "skin": + case "order": + case "accessory": + case "nickname": + (this.players[playerId] as any)[event.property] = event.newValue; + break; + case "scale": + this.players[playerId].scaleFactor = parseFloat(event.newValue); + break; + case "rotation": + (this.players[playerId] as any)[event.property] = parseFloat(event.newValue); + break; + case "animation": + this.players[playerId].changeAnimation(event.newValue); + break; + case "say": + this.players[playerId].say(event.newValue); + break; + case "say-duration": + this.players[playerId].sayDuration = parseInt(event.newValue); + break; + default: + console.warn(`PlayerManager : ${event.property} was not a recognized property.`) + break; + } + this.updatePlayerPositions(); + } + + updatePlayerPositions() { + var playerDistScaled = this.arcDistPlayers; + var teamDistScaled = this.arcDistTeams; + var scale = Player.model_scale; + var numPlayers = Object.keys(this.players).length; + var numTeams = Object.keys(this.teams).length; + if ( this.defaultTeam.players.length === 0 && this.defaultTeam.name in this.teams ) {numTeams -= 1;} + + // Scale player size & distance between players if there are too many players/teams + let totalDist = this.arcDistPlayers * (numPlayers + numTeams - 2) + this.arcDistTeams * (numTeams - 1); + let maxDist = this.circleRadius * this.circleMaxAngle; + let scaleFactor = totalDist / maxDist; + if (scaleFactor > 1) { + teamDistScaled = this.arcDistTeams/scaleFactor; + playerDistScaled = this.arcDistPlayers/scaleFactor; + scale /= scaleFactor; + } + + // Angle between players based on arc distance and circle radius + var theta = playerDistScaled / this.circleRadius; + + // Angle between teams + var thetaTeams = teamDistScaled / this.circleRadius; + + // Get starting angle based on num players + var angle = Math.PI/2 - (theta/2 * (numPlayers-1)) - (thetaTeams/2 * (numTeams-1)); + + for (var teamName in this.teams) { + var len = this.teams[teamName].players.length; + if (len !== 0) { + // Sort players by order within the team + let sortedPlayers = this.teams[teamName].players.sort((obj1,obj2)=>(obj1.order >= obj2.order?1:-1)); + //let sortedPlayers = this.teams[teamName].players + // Set player positions + sortedPlayers.forEach(player => { + player.rotation = new Vector3(0, -angle - Math.PI/2, 0); + player.position = new Vector3(Math.cos(angle), 0, Math.sin(angle)).multiplyScalar(this.circleRadius); + player.scale = scale; + angle += theta; + }); + + // Set team nametag position + let pos = this.teams[teamName].players[Math.floor(len/2)].position.clone(); + pos.y -= 1; + this.teams[teamName].nameTag.position = pos; + + angle += thetaTeams; + } + } + } + + hasPlayer(id: string) : boolean{ + return id in this.players; + } + + addPlayer(id: string, refresh: boolean) { + if(!this.hasPlayer(id)){ + this.players[id] = new Player(id, this.defaultTeam); + if(refresh){ + this.updatePlayerPositions(); + } + } + } + + removePlayer(id: string) { + this.players[id].destroy(); + delete this.players[id]; + } + + assignTeam(playerName: string, teamName: string) { + console.log(`Adding ${playerName} to ${teamName}`); + if (playerName in this.players) { + if (!(teamName in this.teams)) { + this.teams[teamName] = new Team(teamName); + } + let player = this.players[playerName]; + let oldTeam = player.team; + player.changeTeam(this.teams[teamName]); + if (oldTeam.players.length === 0 && oldTeam !== this.defaultTeam) { + oldTeam.destroy(); + delete this.teams[oldTeam.name]; + } + this.updatePlayerPositions(); + } else { + console.warn(`Player ${playerName} does not exist`); + } + } +} \ No newline at end of file diff --git a/modules/web-ui/src/scoreboard.ts b/modules/web-ui/src/scoreboard.ts new file mode 100644 index 0000000..e1d0419 --- /dev/null +++ b/modules/web-ui/src/scoreboard.ts @@ -0,0 +1,83 @@ +import * as THREE from "three"; +import { Context } from "./updateObject"; +import { MqttMicrosquadEventType, MqttUpdateEvent } from "./mqtt"; +import { Observable } from "rxjs"; + +export class Scoreboard { + mesh: THREE.Mesh; + context: Context; + geometry: THREE.PlaneGeometry; + material: THREE.MeshBasicMaterial; + height = 11; + position = new THREE.Vector3(0, 6.5, 14); + rotation = new THREE.Euler(0, Math.PI, 0); + + constructor(context: Context, observable: Observable) { + this.context = context; + observable.subscribe(this.observer); + } + + observer = { + next: (event) => {this.handleMQTTUpdateEvent(event)}, + error: err => console.log("Error handling MQTT Update Event "+err) + } + + handleMQTTUpdateEvent(event : MqttUpdateEvent){ + if(event.type === MqttMicrosquadEventType.SCOREBOARD_UPDATE){ + switch(event.property){ + case "show": + if(this.mesh){ + this.mesh.visible = Boolean(event.newValue).valueOf(); + } + break; + case "image": + this.setBase64Image(event.newValue); + break; + case "score": + // TODO: Superimpose score over image, if not empty + break; + default: + console.log("Unhandled scoreboard property :"+event.property); + } + } + + } + + setBase64Image(base64Image: string) { + // Dispose existing resources + if (this.mesh) { + this.context.scene.remove( this.mesh ); + } + if (this.geometry) { + this.geometry.dispose(); + } + if (this.material) { + this.material.dispose(); + } + + // Generate image element from b64 + let image: HTMLImageElement = new Image(); + image.src = base64Image; + let texture = new THREE.Texture(); + texture.image = image; + + image.onload = () => { + texture.needsUpdate = true; + + texture.wrapS = texture.wrapT = THREE.MirroredRepeatWrapping; + + // Create mesh + this.geometry = new THREE.PlaneGeometry(this.height * image.width / image.height, this.height); + this.material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true + }); + this.mesh = new THREE.Mesh(this.geometry, this.material); + this.mesh.visible = true; + + this.mesh.position.copy(this.position); + this.mesh.rotation.copy(this.rotation); + this.context.scene.add(this.mesh); + }; + } +} diff --git a/modules/web-ui/src/team.ts b/modules/web-ui/src/team.ts new file mode 100644 index 0000000..433a938 --- /dev/null +++ b/modules/web-ui/src/team.ts @@ -0,0 +1,29 @@ +import { Vector3 } from "three"; +import { TextBox3D } from "./textbox3D"; +import { Player } from "./player"; + +export class Team { + name: string; + players: Player[]; + nameTag: TextBox3D; + + constructor (name: string, players?: Player[], disableNameTag?: boolean) { + this.name = name; + this.players = players || []; + + this.nameTag = new TextBox3D(name, new Vector3(0, 0, 0)); + this.nameTag.visible = !(disableNameTag || false); + } + + addPlayer(player: Player) { + this.players.push(player); + } + + removePlayer(player: Player) { + this.players = this.players.filter((p) => p !== player); + } + + destroy() { + this.nameTag.destroy(); + } +} \ No newline at end of file diff --git a/modules/web-ui/src/textbox3D.ts b/modules/web-ui/src/textbox3D.ts new file mode 100644 index 0000000..604551f --- /dev/null +++ b/modules/web-ui/src/textbox3D.ts @@ -0,0 +1,96 @@ +import { Vector3 } from "three"; +import { UpdateObject } from "./updateObject"; + +export class TextBox3D extends UpdateObject { + static canvas: HTMLCanvasElement; + + position: Vector3; + textElement: HTMLDivElement; + textOffsetWidth: number; + textOffsetHeight: number; + hasTriangle: boolean; + triangleElement: HTMLDivElement; + triangleSize = 10; + color = '#FFFFFF99'; + + private _visible: boolean = true; + + constructor (text: string,position: Vector3, hasTriangle?: boolean, fontSize: string = "34px") { + super(); + this.position = position; + TextBox3D.canvas = UpdateObject.context.renderer.domElement; + + + this.textElement = document.createElement('div'); + this.textElement.style.position = 'absolute'; + this.textElement.style.width = 'fit-content'; + this.textElement.style.height = 'fit-content'; + this.textElement.style.padding = '5px'; + this.textElement.style.fontSize = fontSize; + this.textElement.style.backgroundColor = this.color; + this.textElement.style.borderRadius = '15px'; + this.textElement.innerHTML = text; + document.body.appendChild(this.textElement); + this.textOffsetWidth = this.textElement.offsetWidth; + this.textOffsetHeight = this.textElement.offsetHeight; + + this.hasTriangle = hasTriangle || false; + if (this.hasTriangle) { + this.triangleElement = document.createElement('div'); + this.triangleElement.style.position = 'absolute'; + this.triangleElement.style.width = '0'; + this.triangleElement.style.height = '0'; + this.triangleElement.style.borderLeft = this.triangleSize + 'px solid transparent'; + this.triangleElement.style.borderRight = this.triangleSize + 'px solid transparent'; + this.triangleElement.style.borderTop = this.triangleSize * 1.5 + 'px solid ' + this.color; + document.body.appendChild(this.triangleElement); + } + } + + + update(delta : number) { + if (!this._visible) { return; } + var position2D = new Vector3().copy(this.position); + // map to normalized device coordinate (NDC) space + position2D.project( UpdateObject.context.camera ); + + // map to 2D screen space + position2D.x = Math.round( ( position2D.x + 1 ) * TextBox3D.canvas.width / 2 ); + position2D.y = Math.round( ( - position2D.y + 1 ) * TextBox3D.canvas.height / 2 ); + + var elemCoords = { + x: position2D.x - this.textOffsetWidth / 2, + y: position2D.y - this.textOffsetHeight /2 + } + this.textElement.style.left = elemCoords.x + 'px'; + this.textElement.style.top = elemCoords.y + 'px'; + + if (this.hasTriangle) { + this.triangleElement.style.left = elemCoords.x + (this.textOffsetWidth / 2 - this.triangleSize) + 'px'; + this.triangleElement.style.top = elemCoords.y + (this.textOffsetHeight) + 'px'; + } + // TODO change fontSize & triangleSize based on distance to camera + // TODO change zindex based on depth (minor) + } + + set visible(val: boolean) { + if (this._visible !== val) { + this._visible = val; + if (val) { + document.body.appendChild(this.textElement); + } else { + document.body.removeChild(this.textElement); + } + } + } + + get visible() { + return this._visible; + } + + destroy() { + if (this.textElement) { this.textElement.remove(); } + if (this.triangleElement) { this.triangleElement.remove(); } + super.destroy(); + } +} \ No newline at end of file diff --git a/modules/web-ui/src/updateObject.ts b/modules/web-ui/src/updateObject.ts new file mode 100644 index 0000000..370b2f3 --- /dev/null +++ b/modules/web-ui/src/updateObject.ts @@ -0,0 +1,25 @@ +import { Camera, Renderer, Scene } from "three"; + +export interface Context { + scene : Scene; + camera: Camera; + renderer: Renderer; + objList : UpdateObject[]; +} + +export abstract class UpdateObject { + static context : Context; + + constructor () { + UpdateObject.context.objList.push(this); + } + + abstract update(delta : number): void; + + destroy() { + var index = UpdateObject.context.objList.indexOf(this); + if (index !== -1) { + UpdateObject.context.objList.splice(index, 1); + } + } +} \ No newline at end of file diff --git a/modules/web-ui/tests/conf/nginx/default.conf.template b/modules/web-ui/tests/conf/nginx/default.conf.template new file mode 100644 index 0000000..f2b20ad --- /dev/null +++ b/modules/web-ui/tests/conf/nginx/default.conf.template @@ -0,0 +1,12 @@ +server{ + listen 8080; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/modules/web-ui/tsconfig.json b/modules/web-ui/tsconfig.json new file mode 100644 index 0000000..7793360 --- /dev/null +++ b/modules/web-ui/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es6", + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true + }, + "include": ["./src/**/*.ts"], +} diff --git a/modules/web-ui/webpack.common.js b/modules/web-ui/webpack.common.js new file mode 100644 index 0000000..50c03d8 --- /dev/null +++ b/modules/web-ui/webpack.common.js @@ -0,0 +1,40 @@ +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); +const DotenvWebpackPlugin = require("dotenv-webpack"); + +const ASSET_PATH = process.env.ASSET_PATH || '/'; + +module.exports = { + entry: "./src/app.ts", + output: { + publicPath: "/", + filename: "js/bundle.js", + }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader' + ] + } + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + inject: true, + template: "./public/index.html", + }), + new CleanWebpackPlugin(), + new DotenvWebpackPlugin() + ], +}; \ No newline at end of file diff --git a/modules/web-ui/webpack.dev.js b/modules/web-ui/webpack.dev.js new file mode 100644 index 0000000..0e2e2bb --- /dev/null +++ b/modules/web-ui/webpack.dev.js @@ -0,0 +1,14 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); + +module.exports = merge(common, { + mode: "development", + devServer: { + host: "0.0.0.0", + port: 8000, + disableHostCheck: true, + contentBase: "public", + publicPath: "/", + hot: true, + }, +}); \ No newline at end of file diff --git a/modules/web-ui/webpack.prod.js b/modules/web-ui/webpack.prod.js new file mode 100644 index 0000000..812b091 --- /dev/null +++ b/modules/web-ui/webpack.prod.js @@ -0,0 +1,6 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); + +module.exports = merge(common, { + mode: 'production', +}); \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..41c0f0c --- /dev/null +++ b/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..8611571 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ceb480e --- /dev/null +++ b/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + com.github.cmcrobotics.microsquad + reactor + 0.1.0-SNAPSHOT + pom + + Microsquad :: reactor project + ${project.name} + + https://github.com/cmcrobotics/microsquad + + + CERN Micro Club + http://cern.ch/cmc + + + + + GPLv3 + https://opensource.org/licenses/GPL-3.0 + repo + + + + + https://github.com/cmcrobotics/microsquad + scm:git:git://github.com/cmcrobotics/microsquad.git + scm:git:ssh://git@github.com/cmcrobotics/microsquad.git + HEAD + + + + v14.15.4 + 6.14.10 + + + + + + org.apache.maven.plugins + maven-release-plugin + + v{project.version} + + + + com.amashchenko.maven.plugin + gitflow-maven-plugin + 1.11.0 + + + org.codehaus.mojo + versions-maven-plugin + 2.5 + + false + true + true + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-versions + + enforce + + + + + 3.2.5 + + + + + + + + + + + + + + diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..3fb75a6 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,2 @@ +sonar.projectKey=cmcrobotics_microsquad_AXqFHl3_GwvrFd_-6x6l +sonar.qualitygate.wait=true