Compare commits
81 Commits
spotify-in
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e6cfe21d8 | ||
|
|
5d72159cb2 | ||
|
|
8823eac094 | ||
|
|
6d93f3dcad | ||
|
|
7dc7a92dd1 | ||
|
|
b2821d412c | ||
|
|
46b78dd6b3 | ||
|
|
98ed84aa2b | ||
|
|
bcefe03c50 | ||
|
|
e9b3b630b6 | ||
|
|
efd5472eed | ||
|
|
3c8e005405 | ||
|
|
61a376cfbb | ||
|
|
18350ee878 | ||
|
|
aaf33d55db | ||
|
|
0b167aaa35 | ||
|
|
c3adb66de8 | ||
|
|
f8147ffb14 | ||
|
|
bc74978a79 | ||
|
|
437206851b | ||
|
|
80b21e5073 | ||
|
|
fd68a02503 | ||
|
|
c2962bac16 | ||
|
|
d74aebfda7 | ||
|
|
9661ba92d5 | ||
|
|
e53360e887 | ||
|
|
0ad4265bed | ||
|
|
19c8f4fa85 | ||
|
|
abdc894359 | ||
|
|
30f8307551 | ||
|
|
194d74f4e7 | ||
|
|
0528e71ad8 | ||
|
|
fa70efaa98 | ||
|
|
313ca7c677 | ||
|
|
2ac5050284 | ||
|
|
30d660ad7a | ||
|
|
b7a72f6792 | ||
|
|
8730b90039 | ||
|
|
76403d3821 | ||
|
|
67fc7edbb5 | ||
|
|
ca00e52377 | ||
|
|
01cc04f0b2 | ||
|
|
aa83901435 | ||
|
|
60db93dd2d | ||
|
|
2b3027c496 | ||
|
|
7b92e139a7 | ||
|
|
6bf997e3be | ||
|
|
48be847d08 | ||
|
|
75809a310a | ||
|
|
f87d40948f | ||
|
|
21d9ed8602 | ||
|
|
3f3da329b4 | ||
|
|
416f258f00 | ||
|
|
c2a0d955c3 | ||
|
|
680ece01a4 | ||
|
|
39d8230355 | ||
|
|
34b86fff09 | ||
|
|
fb7aaa5c84 | ||
|
|
1ac1cc4278 | ||
|
|
b1d3114921 | ||
|
|
b5da012a34 | ||
|
|
5c2b2a6ef3 | ||
|
|
acc5630e42 | ||
|
|
663fa2b628 | ||
|
|
080d4fa06f | ||
|
|
5d2595020a | ||
|
|
42013800da | ||
|
|
f2bf3a7ad5 | ||
|
|
68d8d67fc5 | ||
|
|
de193c1099 | ||
|
|
11ccf684b5 | ||
|
|
0f6271c507 | ||
|
|
13594f7e3a | ||
|
|
1ab648903e | ||
|
|
24a1451b45 | ||
|
|
bf3ccca868 | ||
|
|
5f036227f2 | ||
|
|
1fe5a35044 | ||
|
|
baa35f2b7e | ||
|
|
22eba12c0c | ||
|
|
fd6ea2f926 |
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
.DS_Store
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
CLAUDE.md
|
||||
todo.todo
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -47,10 +47,15 @@ Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Ignores
|
||||
node_modules/*
|
||||
logs/*
|
||||
node_modules
|
||||
logs
|
||||
data/
|
||||
|
||||
start.bat
|
||||
tokens.json
|
||||
|
||||
config.json
|
||||
seed.json
|
||||
!seed.example.json
|
||||
.env
|
||||
|
||||
*.todo
|
||||
139
CLAUDE.md
Normal file
139
CLAUDE.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a modern Discord bot built with Discord.js v14 that provides sound effects (both prefix and slash commands), text commands, fun facts, and scheduled events functionality for Discord servers. The bot uses SQLite database for dynamic guild configuration management and is designed for public distribution.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Running the Bot
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Local Node.js execution (requires Node 20+ LTS)
|
||||
pnpm dev # Development mode with auto-reload
|
||||
pnpm start:prod # Production mode (local Node.js)
|
||||
|
||||
# Docker Compose (recommended for production)
|
||||
pnpm start # Start bot with Docker Compose
|
||||
pnpm stop # Stop bot
|
||||
pnpm restart # Restart bot (useful after config changes)
|
||||
pnpm logs # View logs
|
||||
pnpm build # Rebuild image
|
||||
pnpm boom # Quick rebuild and restart
|
||||
|
||||
# Direct Docker (alternative)
|
||||
pnpm image:build # Build Docker image
|
||||
pnpm image:run # Run Docker container with auto-restart
|
||||
```
|
||||
|
||||
### Docker Management
|
||||
|
||||
```bash
|
||||
# Docker Compose approach (recommended)
|
||||
pnpm start && pnpm logs # Start and follow logs
|
||||
pnpm restart # Restart after config changes
|
||||
pnpm boom # Quick rebuild and restart after code changes
|
||||
|
||||
# Direct Docker approach
|
||||
docker stop discord-bot && docker rm discord-bot
|
||||
docker logs discord-bot -f
|
||||
pnpm image:build && pnpm image:run
|
||||
```
|
||||
|
||||
## Architecture & Key Components
|
||||
|
||||
### Core Structure
|
||||
|
||||
- **src/index.js**: Main bot entry point with Discord.js v14 client and event handlers
|
||||
- **src/config/**: Configuration management
|
||||
- `config.js`: Hybrid config manager (SQLite database + file fallback)
|
||||
- `intents.js`: Discord gateway intents
|
||||
- **src/commands/**:
|
||||
- `prefix/`: Traditional prefix commands (!sfx, !funfact, !hamfact, !role, !dance, !join, !leave, !reboot)
|
||||
- `slash/`: Modern slash commands (/sfx with autocomplete, /config with subcommands)
|
||||
- **src/services/**:
|
||||
- `databaseService.js`: SQLite database operations and guild management
|
||||
- `voiceService.js`: Voice connections using @discordjs/voice
|
||||
- `commandLoader.js`: Static/Ankhbot command loader with hot-reload
|
||||
- `sfxManager.js`: Sound effect file management and caching
|
||||
- `schedulerService.js`: Scheduled events handler with timezone support
|
||||
- **src/utils/**: Helper functions (randElement, chunkSubstr, etc.)
|
||||
- **data/**: SQLite database directory (auto-created, mounted in Docker)
|
||||
|
||||
### Database System
|
||||
|
||||
**SQLite Database (data/ghbot.db):**
|
||||
- **guilds table**: Per-server configurations with soft delete support
|
||||
- **scheduled_events table**: Cron jobs with JSON schedule storage
|
||||
- **bot_config table**: Global bot settings (activities, admin, blacklist)
|
||||
|
||||
**Configuration Flow:**
|
||||
1. **Database primary**: Live configurations stored in SQLite
|
||||
2. **File fallback**: config.json used for initial seeding and token storage
|
||||
3. **Auto-migration**: Existing config.json guilds imported on first run
|
||||
4. **Live updates**: `/config` slash commands update database directly
|
||||
|
||||
### Command System
|
||||
|
||||
Commands are handled in priority order:
|
||||
1. **Prefix commands**: Modular commands in src/commands/prefix/
|
||||
2. **Static text commands**: From conf/text_commands (pipe-delimited with aliases)
|
||||
3. **Ankhbot commands**: From conf/ghbot.abcomg (legacy format support)
|
||||
|
||||
### Sound Effects System
|
||||
|
||||
- **Dual interfaces**: Both !sfx prefix command and /sfx slash command with autocomplete
|
||||
- **Auto-discovery**: Sound files in sfx/ directory (.mp3/.wav) automatically available
|
||||
- **Voice integration**: @discordjs/voice with connection pooling and cleanup
|
||||
- **Configuration**: Volume and allowed channels configurable per guild via database
|
||||
|
||||
### Guild Management
|
||||
|
||||
**Auto-Registration System:**
|
||||
- **GuildCreate event**: New guilds get default config + welcome message
|
||||
- **GuildDelete event**: Soft delete preserves settings for re-invite
|
||||
- **Welcome messages**: Different messages for new vs returning guilds
|
||||
- **Slash command registration**: Automatic per-guild registration
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- **conf/text_commands**: Pipe-delimited text commands with alias support
|
||||
- **conf/funfacts & conf/hamfacts**: Line-separated fact collections with hot-reload
|
||||
- **conf/ghbot.abcomg**: Ankhbot command database (JSON format, legacy eval support)
|
||||
- **config.json**: Initial configuration and bot token (optional after seeding)
|
||||
|
||||
### Scheduled Events
|
||||
|
||||
**Storage**: Database with JSON schedule format support
|
||||
**Formats**:
|
||||
- Object format: `{"hour": 7, "minute": 30, "tz": "America/Los_Angeles"}`
|
||||
- Cron format: `"0 9 * * *"`
|
||||
**Features**: Timezone support, role pings, channel targeting
|
||||
|
||||
### Docker Setup
|
||||
|
||||
- **Base image**: Node 20 full Debian (for better SQLite compatibility)
|
||||
- **Package manager**: npm for Docker builds (bypasses pnpm security restrictions)
|
||||
- **Multi-stage**: Single-stage build for simplicity and reliability
|
||||
- **Persistence**: data/ volume for SQLite database
|
||||
- **Dependencies**: All audio libraries and native modules properly compiled
|
||||
|
||||
## Important Implementation Details
|
||||
|
||||
- **Discord.js v14** with modern API patterns and explicit intents
|
||||
- **@discordjs/voice** for audio playback with connection pooling
|
||||
- **SQLite database** with better-sqlite3 for persistent configuration
|
||||
- **Hybrid command system**: Traditional prefix commands + modern slash commands
|
||||
- **Auto-registration**: Public bot ready - automatically configures new guilds
|
||||
- **Soft delete system**: Guild settings preserved when bot is removed/re-added
|
||||
- **Live configuration**: `/config` slash commands for real-time settings updates
|
||||
- **Hot-reload**: Static commands and facts reload without restart
|
||||
- **Voice system**: Modern @discordjs/voice with proper cleanup and error handling
|
||||
- **Admin permissions**: Configuration changes require Administrator Discord permission
|
||||
- **Logging**: Comprehensive logging for commands, SFX usage, and system events
|
||||
- **Error handling**: Graceful handling of missing files, permissions, and malformed data
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Use Node 20 LTS with full Debian for better compatibility
|
||||
FROM node:20
|
||||
|
||||
RUN apt update && apt install -y sqlite3
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files (npm will work better for native modules in Docker)
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies using npm (no security restrictions like pnpm)
|
||||
RUN npm install --production
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Start the bot
|
||||
CMD [ "node", "src/index.js" ]
|
||||
473
README.md
Normal file
473
README.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# GHBot - Discord Sound Effects Bot
|
||||
|
||||
A modern Discord bot built with Discord.js v14 that provides sound effects, text commands, fun facts, and scheduled events for Discord servers.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔊 Sound Effects
|
||||
|
||||
- **Prefix commands**: `!sfx <sound>` - Classic text-based commands
|
||||
- **Slash commands**: `/sfx` with autocomplete - Modern Discord UI with searchable sound effects
|
||||
- Automatic sound discovery from the `sfx/` directory
|
||||
- Configurable volume and channel restrictions per guild
|
||||
|
||||
### 💬 Text Commands
|
||||
|
||||
- **Fun Facts**: Random or specific fact retrieval (`!funfact [number]`)
|
||||
- **Ham Facts**: Ham-related facts (`!hamfact [number]`)
|
||||
- **Static Commands**: Custom text responses loaded from configuration files
|
||||
- **Ankhbot Import**: Support for imported Ankhbot command databases
|
||||
|
||||
### 🎭 Interactive Features
|
||||
|
||||
- **Role Management**: Self-service role assignment (`!role add/remove <role>`)
|
||||
- **Dance Command**: ASCII art dance animation
|
||||
- **Voice Controls**: Join/leave voice channels (`!join`, `!leave`)
|
||||
|
||||
### ⏰ Scheduling
|
||||
|
||||
- **Scheduled Events**: Cron-based message scheduling with role pings
|
||||
- **Activity Rotation**: Automatic bot status updates
|
||||
|
||||
### 🛠️ Admin Features
|
||||
|
||||
- **Dynamic Configuration**: SQLite database for persistent, per-server settings
|
||||
- **Auto-Registration**: New guilds automatically configured when bot is added
|
||||
- **Live Configuration**: `/config` slash commands for real-time settings updates
|
||||
- **Soft Delete**: Guild settings preserved when bot is temporarily removed
|
||||
- **Hot Reload**: Configuration files update without restart
|
||||
- **Admin Commands**: Bot management and restart capabilities
|
||||
- **Blacklist System**: Block specific users from using commands
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20 LTS (for Docker) or 22+ LTS (for local development)
|
||||
- pnpm package manager
|
||||
- Discord Bot Token (see Discord Setup section below)
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Clone the repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/greenham/ghbot.git
|
||||
cd ghbot
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **Configure the bot**
|
||||
|
||||
```bash
|
||||
cp config.example.json config.json
|
||||
# Edit config.json with your bot token and existing guild settings (optional)
|
||||
```
|
||||
|
||||
**Note**: The bot uses a SQLite database for guild configurations. If you have existing guilds in `config.json`, they will be automatically imported to the database on first run. For new deployments, guilds are auto-registered when the bot is added to servers.
|
||||
|
||||
4. **Set up Discord Bot** (see Discord Setup section below)
|
||||
|
||||
5. **Run the bot**
|
||||
|
||||
```bash
|
||||
# Development mode with auto-reload (local Node.js)
|
||||
pnpm dev
|
||||
|
||||
# Production mode with Docker Compose
|
||||
pnpm start
|
||||
|
||||
# Production mode with local Node.js
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
### Recommended: Docker Compose
|
||||
|
||||
```bash
|
||||
# Start the bot with Docker Compose
|
||||
pnpm start
|
||||
|
||||
# View logs
|
||||
pnpm logs
|
||||
|
||||
# Restart the bot (useful after config changes)
|
||||
pnpm restart
|
||||
|
||||
# Stop the bot
|
||||
pnpm stop
|
||||
|
||||
# Quick rebuild and restart (after code changes)
|
||||
pnpm boom
|
||||
|
||||
# Manual build and start
|
||||
pnpm build && pnpm start
|
||||
```
|
||||
|
||||
**Benefits of Docker Compose:**
|
||||
|
||||
- Update `config.json`, `sfx/`, and `conf/` files without rebuilding the image
|
||||
- SQLite database persistence via mounted `./data` volume
|
||||
- Automatic restart on failure
|
||||
- Easy log management
|
||||
- Resource limits and health checks
|
||||
|
||||
### Alternative: Direct Docker
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
pnpm image:build
|
||||
|
||||
# Run the container
|
||||
pnpm image:run
|
||||
```
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
### Sound Effects
|
||||
|
||||
**Prefix Command:**
|
||||
|
||||
```
|
||||
!sfx albert # Play 'albert' sound effect
|
||||
!sfx # List all available sounds
|
||||
```
|
||||
|
||||
**Slash Command:**
|
||||
|
||||
```
|
||||
/sfx sound: albert # Play with autocomplete suggestions
|
||||
```
|
||||
|
||||
### Text Commands
|
||||
|
||||
```
|
||||
!funfact # Random fun fact
|
||||
!funfact 42 # Specific fun fact #42
|
||||
!hamfact # Random ham fact
|
||||
!dance # ASCII dance animation
|
||||
```
|
||||
|
||||
### Role Management
|
||||
|
||||
```
|
||||
!role add streamer # Add the 'streamer' role
|
||||
!role remove vip # Remove the 'vip' role
|
||||
```
|
||||
|
||||
### Voice Commands
|
||||
|
||||
```
|
||||
!join # Join your voice channel
|
||||
!leave # Leave current voice channel
|
||||
```
|
||||
|
||||
### Configuration Management
|
||||
|
||||
**Dynamic Configuration (Administrator only):**
|
||||
|
||||
```
|
||||
/config show # View current server settings
|
||||
/config prefix ! # Set command prefix
|
||||
/config sfx true # Enable/disable sound effects
|
||||
/config volume 0.8 # Set SFX volume (0.1-1.0)
|
||||
/config funfacts true # Enable/disable fun facts
|
||||
/config hamfacts true # Enable/disable ham facts
|
||||
/config sfxchannels general|music # Set allowed SFX channels (regex)
|
||||
/config roles streamer|vip|member # Set self-assignable roles
|
||||
```
|
||||
|
||||
## 🔧 Discord Setup
|
||||
|
||||
### Creating a Discord Bot
|
||||
|
||||
1. **Go to Discord Developer Portal**
|
||||
|
||||
- Visit https://discord.com/developers/applications
|
||||
- Click "New Application" and give it a name
|
||||
|
||||
2. **Create Bot User**
|
||||
|
||||
- Go to the "Bot" section
|
||||
- Click "Add Bot"
|
||||
- Copy the bot token for your config.json
|
||||
|
||||
3. **Enable Required Intents**
|
||||
Under "Privileged Gateway Intents", enable:
|
||||
|
||||
- **SERVER MEMBERS INTENT** (Required for role management)
|
||||
- **MESSAGE CONTENT INTENT** (Required for prefix commands)
|
||||
|
||||
4. **Bot Permissions**
|
||||
When inviting the bot, ensure it has these permissions:
|
||||
|
||||
- Send Messages
|
||||
- Embed Links
|
||||
- Read Message History
|
||||
- Connect (for voice channels)
|
||||
- Speak (for voice channels)
|
||||
- Use Voice Activity
|
||||
- Manage Roles (if using role commands)
|
||||
|
||||
5. **Invite Bot to Server**
|
||||
- Go to "OAuth2 > URL Generator"
|
||||
- Select "bot" and "applications.commands" scopes
|
||||
- Select the permissions listed above
|
||||
- Use the generated URL to invite your bot
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Database-Driven Configuration
|
||||
|
||||
The bot uses **SQLite database** for persistent guild configurations. Configuration can be managed in three ways:
|
||||
|
||||
#### 1. Automatic Registration (Recommended for Public Bot)
|
||||
|
||||
When the bot is added to a new server, it automatically:
|
||||
|
||||
- Creates default configuration with sensible settings
|
||||
- Sends a welcome message explaining features
|
||||
- Registers slash commands for the server
|
||||
|
||||
#### 2. Live Configuration via Slash Commands
|
||||
|
||||
Administrators can use `/config` commands to modify settings in real-time:
|
||||
|
||||
- `/config show` - View current server settings
|
||||
- `/config prefix <prefix>` - Change command prefix
|
||||
- `/config sfx <true/false>` - Enable/disable sound effects
|
||||
- And more (see Configuration Management section above)
|
||||
|
||||
#### 3. Seed Data from config.json (Optional)
|
||||
|
||||
For initial deployment or migrating existing servers, create `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"botName": "YourBot",
|
||||
"debug": false,
|
||||
"discord": {
|
||||
"token": "YOUR_BOT_TOKEN",
|
||||
"adminUserId": "YOUR_DISCORD_USER_ID",
|
||||
"guilds": [
|
||||
{
|
||||
"id": "GUILD_ID",
|
||||
"internalName": "My Server",
|
||||
"prefix": "!",
|
||||
"enableSfx": true,
|
||||
"sfxVolume": 0.5,
|
||||
"enableFunFacts": true,
|
||||
"enableHamFacts": true,
|
||||
"scheduledEvents": [
|
||||
{
|
||||
"id": "daily-reminder",
|
||||
"schedule": {
|
||||
"hour": 7,
|
||||
"minute": 30,
|
||||
"tz": "America/Los_Angeles"
|
||||
},
|
||||
"channelId": "CHANNEL_ID",
|
||||
"message": "Good morning!",
|
||||
"pingRoleId": "ROLE_ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"activities": ["Playing sounds", "Serving facts"],
|
||||
"blacklistedUsers": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Migration Process**: On first startup with an empty database, the bot will automatically import all guilds and settings from `config.json` into the database. After migration, the database becomes the primary configuration source.
|
||||
|
||||
### Sound Effects Setup
|
||||
|
||||
1. Add `.mp3` or `.wav` files to the `sfx/` directory
|
||||
2. Files are automatically discovered (filename becomes command name)
|
||||
3. Sounds appear in slash command autocomplete
|
||||
|
||||
### Static Commands
|
||||
|
||||
Edit `conf/text_commands` with pipe-separated commands:
|
||||
|
||||
```
|
||||
hello|Hello there!
|
||||
wiki,wikipedia|https://wikipedia.org
|
||||
help,commands|Available commands: !sfx, !funfact, !hamfact
|
||||
```
|
||||
|
||||
### Scheduled Events (Advanced)
|
||||
|
||||
**For config.json seeding only** - Scheduled events are stored in the database:
|
||||
|
||||
```json
|
||||
"scheduledEvents": [
|
||||
{
|
||||
"id": "daily-greeting",
|
||||
"schedule": {
|
||||
"hour": 9,
|
||||
"minute": 0,
|
||||
"tz": "America/New_York"
|
||||
},
|
||||
"channelId": "CHANNEL_ID",
|
||||
"message": "Good morning everyone!",
|
||||
"pingRoleId": "ROLE_ID"
|
||||
},
|
||||
{
|
||||
"id": "weekly-reminder",
|
||||
"schedule": "0 10 * * 1",
|
||||
"channelId": "CHANNEL_ID",
|
||||
"message": "Happy Monday!"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Schedule Formats Supported:**
|
||||
|
||||
- **Object format**: `{"hour": 9, "minute": 30, "tz": "America/Los_Angeles"}` (with timezone)
|
||||
- **Cron format**: `"0 9 * * *"` (standard cron expression)
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.js # Main bot entry point
|
||||
├── config/
|
||||
│ ├── config.js # Hybrid configuration manager (database + file)
|
||||
│ └── intents.js # Discord gateway intents
|
||||
├── commands/
|
||||
│ ├── prefix/ # Traditional prefix commands (!sfx, !funfact, etc.)
|
||||
│ └── slash/ # Modern slash commands (/sfx, /config)
|
||||
├── services/
|
||||
│ ├── databaseService.js # SQLite database operations
|
||||
│ ├── voiceService.js # Voice connection management
|
||||
│ ├── commandLoader.js # Static/Ankhbot command loader
|
||||
│ ├── sfxManager.js # Sound effect file management
|
||||
│ └── schedulerService.js # Scheduled events handler
|
||||
└── utils/
|
||||
└── helpers.js # Utility functions
|
||||
|
||||
data/
|
||||
└── ghbot.db # SQLite database (auto-created)
|
||||
```
|
||||
|
||||
## 🧪 Development
|
||||
|
||||
### Running in Development Mode
|
||||
|
||||
```bash
|
||||
pnpm dev # Uses nodemon for auto-reload
|
||||
```
|
||||
|
||||
### Adding New Commands
|
||||
|
||||
**Prefix Command Example:**
|
||||
|
||||
```javascript
|
||||
// src/commands/prefix/hello.js
|
||||
module.exports = {
|
||||
name: "hello",
|
||||
description: "Say hello",
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
await message.channel.send("Hello there!");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Slash Command Example:**
|
||||
|
||||
```javascript
|
||||
// src/commands/slash/ping.js
|
||||
const { SlashCommandBuilder } = require("discord.js");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("ping")
|
||||
.setDescription("Replies with Pong!"),
|
||||
|
||||
async execute(interaction, guildConfig) {
|
||||
await interaction.reply("Pong!");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 📊 System Requirements
|
||||
|
||||
- **Node.js**: 20 LTS (Docker) or 22+ LTS (local development)
|
||||
- **Memory**: 256MB+ RAM
|
||||
- **Storage**: 500MB+ (depending on sound effects and database)
|
||||
- **Network**: Stable internet connection for Discord API
|
||||
- **Database**: SQLite (auto-created, no external database required)
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**"Used disallowed intents" Error**
|
||||
|
||||
- Enable required intents in Discord Developer Portal (see Discord Setup section above)
|
||||
- Ensure SERVER MEMBERS INTENT and MESSAGE CONTENT INTENT are enabled
|
||||
|
||||
**Voice/Audio Issues**
|
||||
|
||||
- Ensure ffmpeg is installed (handled automatically in Docker)
|
||||
- Check bot has Connect and Speak permissions
|
||||
- Verify voice channel isn't full or restricted
|
||||
|
||||
**Slash Commands Not Appearing**
|
||||
|
||||
- Commands register on bot startup
|
||||
- May take up to 1 hour to appear globally
|
||||
- Try restarting the bot
|
||||
|
||||
**Permission Errors**
|
||||
|
||||
- Ensure bot has necessary permissions in channels
|
||||
- Check role hierarchy (bot role should be above managed roles)
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging in `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"debug": true
|
||||
}
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||
3. Commit changes: `git commit -m 'Add amazing feature'`
|
||||
4. Push to branch: `git push origin feature/amazing-feature`
|
||||
5. Open a Pull Request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Built with [Discord.js v14](https://discord.js.org/)
|
||||
- Audio processing via [@discordjs/voice](https://github.com/discordjs/voice)
|
||||
- Inspired by the original AnkhBot command system
|
||||
- Special thanks to the Discord.js community
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- Create an [Issue](https://github.com/greenham/ghbot/issues) for bug reports
|
||||
- Check the Discord Setup section above for configuration help
|
||||
- Review [CLAUDE.md](CLAUDE.md) for development guidance
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ for Discord communities
|
||||
File diff suppressed because one or more lines are too long
12287
conf/rooms.json
12287
conf/rooms.json
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
f|is for :frog:
|
||||
ladeda|https://www.youtube.com/watch?v=V0HCZ4YGqbw
|
||||
fine,ez,steve|https://www.youtube.com/watch?v=_c1NJQ0UP_Q
|
||||
popsicle,popsicles|https://clips.twitch.tv/FrigidTardyNoodleKappaRoss
|
||||
imelly|Didn't know this was a political channel. WTF, you don't have sites where you can vomit that garbage out for the peanut gallery? Not everyone holds your same position. Why do that? Don't assume everyone is a socialist. Knock it off. I didn't join here to listen to your political BS. I am not paying a subscription to listen to you people pontificate about your political hangups with normal people. Unsubscribed, unfollowed. -iMellyGurl
|
||||
dance|*┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛*
|
||||
@@ -1,7 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "vr",
|
||||
"interval": 1800,
|
||||
"value": "Video and room requests are on! Use $vr <video-id> to request a video from this list [https://pastebin.com/qv0wDkvB] or $room <room-id> to request a specific room (looped for a few minutes) from this list [https://goo.gl/qoNmuH]"
|
||||
}
|
||||
]
|
||||
720
conf/vods.json
720
conf/vods.json
@@ -1,720 +0,0 @@
|
||||
{
|
||||
"alttp": [
|
||||
{
|
||||
"id": "ttas",
|
||||
"category": "Optimal TTAS",
|
||||
"label": "Any% NMG Optimal Theory TAS [1:20:38.72]",
|
||||
"name": "OTTAS Full",
|
||||
"chatName": "OTTAS Full",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\full-risky-raw.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 4843,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-escape",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Escape (OTTAS Seg)",
|
||||
"chatName": "Escape (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\01-escape.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 352,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-eastern",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Eastern (OTTAS Seg)",
|
||||
"chatName": "Eastern (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\02-eastern.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 277,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-desert",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Desert (OTTAS Seg)",
|
||||
"chatName": "Desert (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\03-desert.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 345,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-hera",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Hera (OTTAS Seg)",
|
||||
"chatName": "Hera (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\04-hera.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 299,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-atower",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "ATower (OTTAS Seg)",
|
||||
"chatName": "ATower (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\05-atower.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 352,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-pod",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "PoD (OTTAS Seg)",
|
||||
"chatName": "PoD (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\06-pod.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 309,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-thieves",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Thieves (OTTAS Seg)",
|
||||
"chatName": "Thieves (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\07-thieves.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 377,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-skull",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Skull (OTTAS Seg)",
|
||||
"chatName": "Skull (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\08-skull.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 267,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-ice",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Ice (OTTAS Seg)",
|
||||
"chatName": "Ice (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\09-ice.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 318,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-swamp",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Swamp (OTTAS Seg)",
|
||||
"chatName": "Swamp (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\10-swamp.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 345,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-mire",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Mire (OTTAS Seg)",
|
||||
"chatName": "Mire (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\11-mire.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 365,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-trock",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "TRock (OTTAS Seg)",
|
||||
"chatName": "TRock (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\12-trock.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 361,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-gtower",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "GTower (OTTAS Seg)",
|
||||
"chatName": "GTower (OTTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\13-gtower.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 396,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "ot-seg-ganon",
|
||||
"category": "Optimal TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Ganon (TTAS Seg)",
|
||||
"chatName": "Ganon (TTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\final-versions-timed\\14-ganon.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 108,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "rta-ttas",
|
||||
"category": "Safe TTAS",
|
||||
"label": "Any% NMG Safe/RTA Theory TAS [1:21:26.52]",
|
||||
"name": "STTAS Full",
|
||||
"chatName": "STTAS Full",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\full-rta-raw.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 4892,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-escape",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Escape (STTAS Seg)",
|
||||
"chatName": "Escape (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\01-escape.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 354,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-eastern",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Eastern (STTAS Seg)",
|
||||
"chatName": "Eastern (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\02-eastern.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 281,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-desert",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Desert (STTAS Seg)",
|
||||
"chatName": "Desert (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\03-desert.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 347,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-hera",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Hera (STTAS Seg)",
|
||||
"chatName": "Hera (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\04-hera.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 303,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-atower",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Agah Tower (STTAS Seg)",
|
||||
"chatName": "Agah Tower (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\05-atower.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 354,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-pod",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "PoD (STTAS Seg)",
|
||||
"chatName": "PoD (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\06-pod.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 310,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-thieves",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Thieves (STTAS Seg)",
|
||||
"chatName": "Thieves (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\07-thieves.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 379,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-skull",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Skull (STTAS Seg)",
|
||||
"chatName": "Skull (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\08-skull.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 270,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-ice",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Ice (STTAS Seg)",
|
||||
"chatName": "Ice (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\09-ice.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 321,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-swamp",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Swamp (STTAS Seg)",
|
||||
"chatName": "Swamp (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\10-swamp.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 351,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-mire",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "Mire (STTAS Seg)",
|
||||
"chatName": "Mire (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\11-mire.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 370,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-trock",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "TRock (STTAS Seg)",
|
||||
"chatName": "TRock (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\12-trock.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 364,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "st-seg-gtower",
|
||||
"category": "Safe TTAS Segment",
|
||||
"label": false,
|
||||
"name": "GTower (STTAS Seg)",
|
||||
"chatName": "GTower (STTAS Seg)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\final-versions-timed\\13-gtower.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 408,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-escape",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Escape (5:52.43) [2018-09-19]",
|
||||
"name": "Escape (NMG Gold)",
|
||||
"chatName": "Escape (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\01-[552.43]-2018-09-19-escape.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 360,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-eastern",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Eastern (5:00.12) [2018-06-25]",
|
||||
"name": "Eastern (NMG Gold)",
|
||||
"chatName": "Eastern (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\02-[500.12]-2018-06-25-eastern.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 302,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-desert",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Desert (6:10.32) [2018-06-02]",
|
||||
"name": "Desert (NMG Gold)",
|
||||
"chatName": "Desert (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\03-[610.32]-2018-06-02-desert.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 374,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-hera",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Hera (5:28.83) [2018-06-01]",
|
||||
"name": "Hera (NMG Gold)",
|
||||
"chatName": "Hera (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\04-[528.83]-2018-06-01-hera.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 332,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-atower",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Agah Tower (5:13.45) [2018-09-07]",
|
||||
"name": "Agah Tower (NMG Gold)",
|
||||
"chatName": "Agah Tower (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\05a-[513.45]-2018-09-07-atower.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 314,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-pod",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Palace of Darkness (6:11.15) [2018-05-27]",
|
||||
"name": "PoD (NMG Gold)",
|
||||
"chatName": "PoD (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\06-[611.15]-2018-05-27-pod.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 376,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-thieves",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Thieves Town (7:08.37) [2018-07-01]",
|
||||
"name": "Thieves (NMG Gold)",
|
||||
"chatName": "Thieves (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\07-[708.37]-2018-07-01-thieves.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 432,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-skull",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Skull Woods (5:24.28) [2018-06-02]",
|
||||
"name": "Skull (NMG Gold)",
|
||||
"chatName": "Skull (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\08-[524.28]-2018-06-02-skull.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 328,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-ice",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Ice Palace (6:18.13) [2018-09-08]",
|
||||
"name": "Ice (NMG Gold)",
|
||||
"chatName": "Ice (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\09-[618.13]-2018-09-08-ice.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 379,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-swamp",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Swamp Palace (6:51.87) [2018-06-10]",
|
||||
"name": "Swamp (NMG Gold)",
|
||||
"chatName": "Swamp (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\10-[651.87]-2018-06-10-swamp.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 418,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-mire",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Misery Mire (7:06.58) [2018-10-21]",
|
||||
"name": "Mire (NMG Gold)",
|
||||
"chatName": "Mire (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\11-[706.58]-2018-10-21-mire.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 430,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-trock",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Turtle Rock (7:07.34) [2018-06-25]",
|
||||
"name": "TRock (NMG Gold)",
|
||||
"chatName": "TRock (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\12-[707.34]-2018-06-25-trock.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 434,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-gtower",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Ganon's Tower (7:09.85) [2018-08-19]",
|
||||
"name": "GTower (NMG Gold)",
|
||||
"chatName": "GTower (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\13-[709.85]-2018-08-19-gtower.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 433,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nmg-gold-ganon",
|
||||
"category": "Any% NMG Gold Segment",
|
||||
"label": "Any% NMG Gold Segment: Ganon (1:43.06) [2018-05-30]",
|
||||
"name": "Ganon (NMG Gold)",
|
||||
"chatName": "Ganon (NMG Gold)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\14-[143.06]-2018-05-30-ganon.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 117,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-100-ahp",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: 100% All Heart Pieces (1:19:12) [2017-12-22]",
|
||||
"name": "100% MG AHP (PB)",
|
||||
"chatName": "100% MG AHP (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%-mg-ahp\\2017-12-22-100mg-11912.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 4786,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-ab",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: All Bosses No EG (1:09:23) [2017-11-20]",
|
||||
"name": "All Bosses (PB)",
|
||||
"chatName": "All Bosses (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-bosses\\2017-11-20-ab-10923.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 4200,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-ad",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: All Dungeons No EG/DG/WW (1:14:59) [2017-11-19]",
|
||||
"name": "All Dungeons (PB)",
|
||||
"chatName": "All Dungeons (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-dungeons\\2017-11-19-ad-11459.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 4555,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-any-nmg",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: Any% NMG No S+Q (1:26:24) [2018-05-27]",
|
||||
"name": "Any% NMG (PB)",
|
||||
"chatName": "Any% NMG (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-nmg-nsq\\2018-05-27-nmg-12624.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 5190,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-any-no-eg",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: Any% No EG (29:05) [2018-10-14]",
|
||||
"name": "Any% No EG (PB)",
|
||||
"chatName": "Any% No EG (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-no-eg\\2018-10-14-no-eg-2905.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 1777,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-master-sword",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: Master Sword NMG (22:23) [2018-08-23]",
|
||||
"name": "Master Sword (PB)",
|
||||
"chatName": "Master Sword (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\master-sword\\2018-08-23-master-sword-2223.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 1409,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-ms",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: Mirror Shield NMG (50:32) [2017-06-20]",
|
||||
"name": "Mirror Shield (PB)",
|
||||
"chatName": "Mirror Shield (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield\\2017-06-20-mirror-shield-5032.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 3068,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-ms-no-eg",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: Mirror Shield No EG (11:38) [2017-07-02]",
|
||||
"name": "Mirror Shield No EG (PB)",
|
||||
"chatName": "Mirror Shield No EG (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield-no-eg\\2017-07-02-mirror-shield-no-eg-1138-resized.mp4",
|
||||
"sceneItem": "4x3ph",
|
||||
"length": 738,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-rbo",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: Reverse Boss Order (1:18:13) [2017-12-01]",
|
||||
"name": "Reverse Boss Order (PB)",
|
||||
"chatName": "Reverse Boss Order (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\rbo\\2017-12-01-rbo-11813.mp4",
|
||||
"sceneItem": "16x9ph",
|
||||
"length": 4725,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "pb-100-nmg",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: 100% NMG (1:49:30) [2017-01-31]",
|
||||
"name": "100% NMG (PB)",
|
||||
"chatName": "100% NMG (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%\\2017-01-31-hundo-14930.mp4",
|
||||
"sceneItem": "legacyph",
|
||||
"length": 6598,
|
||||
"includeInShuffle": false
|
||||
},
|
||||
{
|
||||
"id": "pb-dg",
|
||||
"category": "Personal Best",
|
||||
"label": "Personal Best: Defeat Ganon (13:41) [2017-02-20]",
|
||||
"name": "Defeat Ganon (PB)",
|
||||
"chatName": "Defeat Ganon (PB)",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\defeat-ganon\\2017-02-20_12-55-40-defeat-ganon-1341.mp4",
|
||||
"sceneItem": "legacyph",
|
||||
"length": 868,
|
||||
"includeInShuffle": true
|
||||
}
|
||||
],
|
||||
"memes": [
|
||||
{
|
||||
"id": "auw",
|
||||
"name": "auw",
|
||||
"filePath": "Y:\\media\\videos\\black-box-everybody-wow.mp4",
|
||||
"sceneItem": "everybody-wow",
|
||||
"length": 247,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "archery",
|
||||
"name": "archery",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\archery-contest.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 27,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "69blazeit",
|
||||
"name": "69blazeit",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\69BlazeIt.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 92,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "rpgfarm",
|
||||
"name": "rpgfarm",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\2016-07-24-1424-06-rpg-race-sniped.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 144,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "emmapeg",
|
||||
"name": "emmapeg",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\emma-pegging.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 8,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "handy",
|
||||
"name": "handy",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\handy-in-the-mothhole.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 39,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "bodyguard",
|
||||
"name": "bodyguard",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\heroic-popo.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 14,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "whowillitbe",
|
||||
"name": "whowillitbe",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\its-gonna-be-may.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 30,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "mindblown",
|
||||
"name": "mindblown",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\mindblowing-and-lifechanging.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 55,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "nerd-nookie",
|
||||
"name": "nerd-nookie",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\memes\\nerd-bizkit.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 27,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "curling",
|
||||
"name": "curling",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\curling-bored-janitors.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 16,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "airplane",
|
||||
"name": "airplane",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\emetaPlane.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 5,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "hard-things",
|
||||
"name": "hard-things",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\questions-about-hard-things.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 39,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "18arrows",
|
||||
"name": "18arrows",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\screevo-18-arrows-fine.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 41,
|
||||
"includeInShuffle": true
|
||||
},
|
||||
{
|
||||
"id": "quake",
|
||||
"name": "quake",
|
||||
"filePath": "Y:\\media\\videos\\ALttP\\trock-indoor-quake.mp4",
|
||||
"sceneItem": "meme1",
|
||||
"length": 17,
|
||||
"includeInShuffle": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"botName": "greenhambot",
|
||||
"d_token": "YOUR DISCORD APP TOKEN",
|
||||
"adminID": "YOUR DISCORD USER ID",
|
||||
"prefix": "!", // prefix that must precede all commands
|
||||
"botChannel": "bot" // default channel where the bot will post things
|
||||
"allowedSfxChannels": "bot", // channels where sfx can be used (separated by pipes)
|
||||
"sfxVolume": 0.3,
|
||||
"passes": 2, // can be increased to reduce packetloss at the expense of upload bandwidth, 4-5 should be lossless at the expense of 4-5x upload
|
||||
"textCmdCooldown": 5 // default cooldown in seconds for all text commands
|
||||
}
|
||||
283
discord.js
283
discord.js
@@ -1,283 +0,0 @@
|
||||
// Import modules
|
||||
const { Client } = require('discord.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
moment = require('moment'),
|
||||
timers = require('./lib/timers.js'),
|
||||
staticCommands = require('./lib/static-commands.js'),
|
||||
//cooldowns = require('./lib/cooldowns.js'),
|
||||
ankhbotCommands = require('./lib/ankhbot-commands.js'),
|
||||
config = require('./config.json');
|
||||
|
||||
// Set up Discord client
|
||||
const client = new Client();
|
||||
|
||||
// Set up SFX
|
||||
const sfxFilePath = path.join(__dirname, 'sfx');
|
||||
const allowedSfxChannels = new RegExp(config.allowedSfxChannels);
|
||||
let playOptions = {volume: config.sfxVolume, passes: config.passes};
|
||||
let playing = false;
|
||||
|
||||
// Read in sfx directory, filenames are the commands
|
||||
let sfxList = readSfxDirectory(sfxFilePath);
|
||||
// Watch directory for changes and update the list
|
||||
fs.watch(sfxFilePath, (eventType, filename) => {
|
||||
if (eventType === 'rename') {
|
||||
sfxList = readSfxDirectory(sfxFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
// @todo DRY this shit up
|
||||
|
||||
// Read in fun facts
|
||||
const funFactsFilePath = path.join(__dirname, 'conf', 'funfacts');
|
||||
let funFacts = parseLines(funFactsFilePath);
|
||||
fs.watchFile(funFactsFilePath, (curr, prev) => {
|
||||
if (curr.mtime !== prev.mtime) {
|
||||
funFacts = parseLines(funFactsFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
// Read in ham facts
|
||||
const hamFactsFilePath = path.join(__dirname, 'conf', 'hamfacts');
|
||||
let hamFacts = parseLines(hamFactsFilePath);
|
||||
fs.watchFile(hamFactsFilePath, (curr, prev) => {
|
||||
if (curr.mtime !== prev.mtime) {
|
||||
hamFacts = parseLines(hamFactsFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up the native commands to handle
|
||||
const commands = {
|
||||
'sfx': (msg, disconnectAfter) => {
|
||||
if (!allowedSfxChannels.test(msg.channel.name)) return;
|
||||
let sfx = msg.content.split(' ')[1];
|
||||
if (sfx == '' || sfx === undefined) return msg.channel.send('```'+sfxList.join(', ')+'```');
|
||||
|
||||
if (playing === true) return msg.channel.send('Already playing, please wait.');
|
||||
|
||||
// make sure this file exists either as an mp3 or wav
|
||||
let sfxPath;
|
||||
if (fs.existsSync(path.join(sfxFilePath, sfx + '.mp3'))) {
|
||||
sfxPath = path.join(sfxFilePath, sfx + '.mp3');
|
||||
} else if (fs.existsSync(path.join(sfxFilePath, sfx + '.wav'))) {
|
||||
sfxPath = path.join(sfxFilePath, sfx + '.wav');
|
||||
} else {
|
||||
return msg.reply('This sound effect does not exist!');
|
||||
}
|
||||
|
||||
if (!msg.guild.voiceConnection) return joinVoiceChannel(msg).then(() => commands.sfx(msg, disconnectAfter));
|
||||
|
||||
disconnectAfter = (typeof disconnectAfter !== "undefined") ? disconnectAfter : true;
|
||||
|
||||
playing = true;
|
||||
(function play(sfxFile) {
|
||||
const dispatcher = msg.guild.voiceConnection.playFile(sfxFile, playOptions);
|
||||
dispatcher.on('end', reason => {
|
||||
playing = false;
|
||||
if (disconnectAfter) msg.guild.voiceConnection.disconnect();
|
||||
})
|
||||
.on('error', error => {
|
||||
playing = false;
|
||||
if (disconnectAfter) msg.guild.voiceConnection.disconnect();
|
||||
})
|
||||
.on('start', () => {});
|
||||
})(sfxPath.toString());
|
||||
},
|
||||
'funfact': (msg) => {
|
||||
if (funFacts.length > 0) {
|
||||
// return random element from funFacts, unless one is specifically requested
|
||||
let el;
|
||||
let req = parseInt(msg.content.split(' ')[1]);
|
||||
if (Number.isNaN(req) || typeof funFacts[req-1] === 'undefined') {
|
||||
el = Math.floor(Math.random() * funFacts.length);
|
||||
} else {
|
||||
el = req - 1;
|
||||
}
|
||||
|
||||
let displayNum = (el+1).toString();
|
||||
let funFact = funFacts[el]
|
||||
msg.channel.send({embed: {
|
||||
"title": "FunFact #"+displayNum,
|
||||
"color": 0x21c629,
|
||||
"description": funFact
|
||||
}}).catch(console.error);
|
||||
} else {
|
||||
msg.channel.send("No fun facts found!");
|
||||
}
|
||||
},
|
||||
'hamfact': (msg) => {
|
||||
if (hamFacts.length > 0) {
|
||||
// return random element from hamFacts, unless one is specifically requested
|
||||
let el;
|
||||
let req = parseInt(msg.content.split(' ')[1]);
|
||||
if (Number.isNaN(req) || typeof hamFacts[req-1] === 'undefined') {
|
||||
el = Math.floor(Math.random() * hamFacts.length);
|
||||
} else {
|
||||
el = req - 1;
|
||||
}
|
||||
|
||||
let displayNum = (el+1).toString();
|
||||
let hamFact = hamFacts[el]
|
||||
msg.channel.send({embed: {
|
||||
"title": "HamFact #"+displayNum,
|
||||
"color": 0x21c629,
|
||||
"description": hamFact
|
||||
}}).catch(console.error);
|
||||
} else {
|
||||
msg.channel.send("No ham facts found!");
|
||||
}
|
||||
},
|
||||
'dance': (msg) => {
|
||||
msg.channel.send("*┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛*");
|
||||
},
|
||||
'join': (msg) => {
|
||||
if (!msg.guild.voiceConnection) {
|
||||
joinVoiceChannel(msg).then(() => {
|
||||
//
|
||||
}).catch(console.error);
|
||||
} else {
|
||||
return msg.reply(`I'm already in a voice channel!`);
|
||||
}
|
||||
},
|
||||
'leave': (msg) => {
|
||||
if (msg.guild.voiceConnection) {
|
||||
msg.content = '!sfx bye';
|
||||
commands.sfx(msg);
|
||||
//msg.guild.voiceConnection.disconnect();
|
||||
} else {
|
||||
return msg.reply(`If ya don't eat your meat, ya can't have any pudding!`);
|
||||
}
|
||||
},
|
||||
'listen': (msg) => {
|
||||
// listen for a particular member to speak and respond appropriately
|
||||
if (msg.guild.voiceConnection) {
|
||||
// get the guild member
|
||||
//let guildMemberId = "88301001169207296"; // me
|
||||
let guildMemberId = "153563292265086977"; // Screevo
|
||||
let guildMember = msg.guild.members.get(guildMemberId);
|
||||
if (guildMember) {
|
||||
let listenInterval = 1000;
|
||||
setInterval(() => {
|
||||
if (guildMember.speaking === true) {
|
||||
msg.content = '!sfx stfu';
|
||||
commands.sfx(msg, false);
|
||||
}
|
||||
}, listenInterval);
|
||||
} else {
|
||||
console.error(`Could not find specified guild member: ${guildMemberId}!`);
|
||||
msg.guild.voiceConnection.disconnect();
|
||||
}
|
||||
} else {
|
||||
// join the voice channel then call this command again
|
||||
joinVoiceChannel(msg).then(() => {
|
||||
commands.listen(msg);
|
||||
}).catch(console.error);
|
||||
}
|
||||
},
|
||||
'reboot': (msg) => {
|
||||
if (msg.author.id == config.adminID) process.exit(); //Requires a node module like Forever to work.
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for discord to be ready, handle messages
|
||||
client.on('ready', () => {
|
||||
console.log(`${config.botName} is connected and ready`);
|
||||
let botChannel = client.channels.find('name', config.botChannel);
|
||||
// Listen for commands for the bot to respond to across all channels
|
||||
}).on('message', msg => {
|
||||
msg.originalContent = msg.content;
|
||||
msg.content = msg.content.toLowerCase();
|
||||
|
||||
// Make sure it starts with the configured prefix
|
||||
if (!msg.content.startsWith(config.prefix)) return;
|
||||
|
||||
// And that it's not on cooldown
|
||||
/*let cooldownKey = config.botName + msg.content + msg.channel.id;
|
||||
cooldowns.get(cooldownKey, config.textCmdCooldown)
|
||||
.then(onCooldown => {
|
||||
if (onCooldown === false) {*/
|
||||
// Not on CD, check for native or static command
|
||||
let commandNoPrefix = msg.content.slice(config.prefix.length).split(' ')[0];
|
||||
console.log(`'${commandNoPrefix}' received in #${msg.channel.name} from @${msg.author.username}`);
|
||||
|
||||
// check for native command first
|
||||
if (commands.hasOwnProperty(commandNoPrefix)) {
|
||||
commands[commandNoPrefix](msg);
|
||||
// then a static command we've manually added
|
||||
} else if (staticCommands.exists(commandNoPrefix)) {
|
||||
let result = staticCommands.get(commandNoPrefix);
|
||||
msg.channel.send({embed: {
|
||||
"title": commandNoPrefix,
|
||||
"color": 0x21c629,
|
||||
"description": result
|
||||
}}).then(sentMessage => {}/*cooldowns.set(cooldownKey, config.textCmdCooldown)*/)
|
||||
.catch(console.error);
|
||||
// then a command exported from ankhbot
|
||||
} else if (ankhbotCommands.exists(commandNoPrefix)) {
|
||||
let result = ankhbotCommands.get(commandNoPrefix);
|
||||
msg.channel.send({embed: {
|
||||
"title": commandNoPrefix,
|
||||
"color": 0x21c629,
|
||||
"description": result
|
||||
}}).then(sentMessage => {}/*cooldowns.set(cooldownKey, config.textCmdCooldown)*/)
|
||||
.catch(console.error);
|
||||
} else {
|
||||
// Not a command we recognize, ignore
|
||||
}
|
||||
/*} else {
|
||||
// DM the user that it's on CD
|
||||
dmUser(msg, `**${msg.content}** is currently on cooldown for another *${onCooldown} seconds!*`);
|
||||
}
|
||||
})
|
||||
.catch(console.error);*/
|
||||
}).login(config.d_token);
|
||||
|
||||
function readSfxDirectory(path)
|
||||
{
|
||||
let sfxList = fs.readdirSync(sfxFilePath);
|
||||
sfxList.forEach(function(el, index, a) {
|
||||
a[index] = el.split('.')[0];
|
||||
});
|
||||
return sfxList;
|
||||
}
|
||||
|
||||
function joinVoiceChannel(msg)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
const voiceChannel = msg.member.voiceChannel;
|
||||
if (!voiceChannel || voiceChannel.type !== 'voice') return msg.reply('I couldn\'t connect to your voice channel...');
|
||||
voiceChannel.join().then(connection => resolve(connection)).catch(err => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
// Read/parse text lines from a file
|
||||
function parseLines(filePath)
|
||||
{
|
||||
let lines = [];
|
||||
let data = fs.readFileSync(filePath, 'utf-8');
|
||||
let splitLines = data.toString().split('\n');
|
||||
splitLines.forEach(function(line) {
|
||||
if (line.length > 0) {
|
||||
lines.push(line);
|
||||
}
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
function dmUser(originalMessage, newMessage)
|
||||
{
|
||||
// check that this isn't already a DM before sending
|
||||
if (originalMessage.channel.type === 'dm') {
|
||||
originalMessage.channel.send(newMessage);
|
||||
} else {
|
||||
originalMessage.member.createDM()
|
||||
.then(channel => {
|
||||
channel.send(newMessage);
|
||||
})
|
||||
.catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
// catch Promise errors
|
||||
process.on('unhandledRejection', console.error);
|
||||
6
docker-compose.override.yml
Normal file
6
docker-compose.override.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
discord-bot:
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
65
docker-compose.yml
Normal file
65
docker-compose.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
discord-bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: discord-bot
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Seed file for initial database population (read-only)
|
||||
- ./seed.json:/app/seed.json:ro
|
||||
- ./conf:/app/conf:ro
|
||||
|
||||
# Sound effects directory (read-only)
|
||||
- ./sfx:/app/sfx:ro
|
||||
|
||||
# Database persistence
|
||||
- ./data:/app/data
|
||||
|
||||
# Optional: Mount logs directory if you want persistent logs
|
||||
# - ./logs:/app/logs
|
||||
|
||||
# Optional: Environment variables
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
# - DEBUG=true # Uncomment for debug mode
|
||||
|
||||
# Optional: Resource limits
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 256M
|
||||
|
||||
# Optional: Health check
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "process.exit(0)"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# Optional: Add a volume backup service
|
||||
# backup:
|
||||
# image: alpine:latest
|
||||
# container_name: ghbot-backup
|
||||
# volumes:
|
||||
# - ./config.json:/backup/config.json:ro
|
||||
# - ./conf:/backup/conf:ro
|
||||
# - ./sfx:/backup/sfx:ro
|
||||
# command: |
|
||||
# sh -c "
|
||||
# echo 'Creating backup...'
|
||||
# tar czf /backup/ghbot-backup-$(date +%Y%m%d_%H%M%S).tar.gz -C /backup config.json conf sfx
|
||||
# echo 'Backup complete'
|
||||
# "
|
||||
# profiles:
|
||||
# - backup
|
||||
|
||||
# Optional: Create named volumes for persistent data
|
||||
# volumes:
|
||||
# ghbot_logs:
|
||||
# driver: local
|
||||
106
fgfm.TODO
106
fgfm.TODO
@@ -1,106 +0,0 @@
|
||||
TODO:
|
||||
☐ Spotify integration
|
||||
✔ song -- display current song + link @done (18-12-04 11:24)
|
||||
✔ playlist -- display current context / album @done (18-12-04 11:24)
|
||||
✔ skip -- mods auto-skip @done (18-12-04 11:47)
|
||||
✔ volume -- volume adjustment @done (18-12-04 19:49)
|
||||
✔ pause / resume @done (18-12-04 11:49)
|
||||
✔ ability to change the playlist (setplaylist <spotify-uri>) @done (18-12-11 10:52)
|
||||
✔ shuffle on/off @done (18-12-11 11:22)
|
||||
✔ repeat mode control @done (18-12-11 11:22)
|
||||
☐ Web interface for viewers to issue commands
|
||||
☐ Organized room / video list, one-click add-to-queue
|
||||
☐ Admin panel on website for control
|
||||
☐ Add support for a command to mute/unmute audio sources
|
||||
☐ Don't re-create queue on start if it already exists
|
||||
☐ Handle socket disconnect
|
||||
☐ Decouple twitch chat from GHOBS
|
||||
☐ Move anything that calls director.state from app into fgfm lib
|
||||
☐ Restrict # of requests a user can have in the queue at once
|
||||
☐ Room vid requests / import
|
||||
☐ Improved interface for viewer requests
|
||||
☐ Web interface? Twitch extension?
|
||||
☐ Improvements
|
||||
☐ When playing a room back, loop it at slower speeds for a few iterations
|
||||
☐ Support adding sets of videos to the queue at once (like the entire ttas or all gold segments)
|
||||
☐ Tool to output list of video ID's / descriptions
|
||||
☐ Stream alerts for chat
|
||||
☐ Rotating background images (leftside)
|
||||
☐ Support gif's via command through gifph
|
||||
- !slowdance
|
||||
- !bender
|
||||
- !carlton
|
||||
- !weeb
|
||||
☐ Fix commercial playing issue (switches back to scene early)
|
||||
☐ Change $auw and $meme to queue up the videos just like the others (don't switch scenes)
|
||||
☐ Stats tracking for games
|
||||
☐ Most won/lost gambling
|
||||
☐ Most trivia answered
|
||||
☐ Allow %'s for !gamble
|
||||
|
||||
Ideas:
|
||||
|
||||
StreamWebRemote:
|
||||
- OBS Websocket Connection Mgmt
|
||||
- Stream Start/Stop/Restart
|
||||
- Video Queue Mgmt
|
||||
- Video DB Mgmt
|
||||
Users:
|
||||
- use twitch login
|
||||
- video requests
|
||||
- room requests
|
||||
|
||||
___________________
|
||||
Archive:
|
||||
✔ Stop @done (18-11-06 21:54) @project(TODO)
|
||||
✔ video starting/ending/skipped @done (18-11-06 21:53) @project(TODO)
|
||||
✔ show status changing @done (18-11-06 21:53) @project(TODO)
|
||||
✔ Have director emit events for bots to listen to @done (18-11-06 21:53) @project(TODO)
|
||||
✔ Auto-enable vrmode timer when show starts (listen for event) @done (18-11-06 21:53) @project(TODO)
|
||||
✔ hide sources before switching to credits @done (18-11-06 21:41) @project(TODO)
|
||||
✔ Support a delay for Start Streaming @done (18-11-06 21:41) @project(TODO)
|
||||
✔ Add a $rooms alias for $room @done (18-11-06 15:05) @project(TODO)
|
||||
✔ Remove currently playing video from vote choices @done (18-10-30 11:51) @project(TODO)
|
||||
✔ track votes for the current playing video @done (18-10-30 11:48) @project(TODO)
|
||||
✔ if threshold is met, skip @done (18-10-30 11:48) @project(TODO)
|
||||
✔ config item for how many votes are required to skip @done (18-10-30 11:48) @project(TODO)
|
||||
✔ clear votes after video finishes @done (18-10-30 11:48) @project(TODO)
|
||||
✔ Support viewer $skip voting @done (18-10-30 11:48) @project(TODO)
|
||||
✔ update director showStatus to 'PAUSED' @done (18-10-30 11:33) @project(TODO)
|
||||
✔ have nextVideo check for PAUSED @done (18-10-30 11:33) @project(TODO)
|
||||
✔ Support for $pause (pauses queue after current video finishes) @done (18-10-30 11:33) @project(TODO)
|
||||
✔ Move vrmode timer to this bot, delete from SLCB @done (18-10-30 11:20) @project(TODO)
|
||||
✔ Don't auto-init GHOBS or FGFM, make them on-demand @done (18-10-30 08:26) @project(TODO)
|
||||
✔ Switch to credits with 1 minute remaining @done (18-10-30 08:15) @project(TODO)
|
||||
✔ Stop Stream @done (18-10-30 08:15) @project(TODO)
|
||||
✔ Stop @done (18-10-30 08:15) @project(TODO)
|
||||
✔ Parameter for how long until the stream should end @done (18-10-30 08:15) @project(TODO)
|
||||
✔ Fade out audio sources with 5 seconds left @done (18-10-30 08:15) @project(TODO)
|
||||
✔ Add parameter for countdown @done (18-10-30 08:15) @project(TODO)
|
||||
✔ Set up the queue upon init so it can be managed during startup @done (18-10-30 08:14) @project(TODO)
|
||||
✔ Starting Soon is shown until countdown is triggered @done (18-10-26 09:44) @project(TODO)
|
||||
✔ Start stream @done (18-10-26 09:44) @project(TODO)
|
||||
✔ Add cooldowns @done (18-10-02 10:16) @project(TODO)
|
||||
✔ video length @done (18-09-28 09:33) @project(TODO)
|
||||
✔ root folder name @done (18-09-28 09:33) @project(TODO)
|
||||
✔ room ID @done (18-09-28 09:33) @project(TODO)
|
||||
✔ original file name @done (18-09-28 09:33) @project(TODO)
|
||||
✔ keywords @done (18-09-28 09:33) @project(TODO)
|
||||
✔ dungeon (parse from root folder name) @done (18-09-28 09:33) @project(TODO)
|
||||
✔ Store the following as metadata: @done (18-09-28 09:33) @project(TODO)
|
||||
✔ Read the Y:\media\videos\ALttP\my-vids\room-vids directory @done (18-09-28 09:33) @project(TODO)
|
||||
✔ Go through each folder and get all the .mp4 files @done (18-09-28 09:33) @project(TODO)
|
||||
✔ Look into just making the video loop instead of hiding at the end @done (18-09-26 18:29) @project(TODO)
|
||||
✔ specify # of loops in video object @done (18-09-26 18:28) @project(TODO)
|
||||
✔ change source to loop? calculate time? @done (18-09-26 18:28) @project(TODO)
|
||||
✔ Update the queue to support looping @done (18-09-26 18:28) @project(TODO)
|
||||
✔ Fix queue to only return first 20 or so @done (18-09-26 12:04) @project(TODO)
|
||||
✔ Importing @done (18-09-26 08:25) @project(TODO)
|
||||
✔ Move vods to their own config @done (18-09-25 15:40) @project(TODO)
|
||||
✔ Modularize OBS and Twitch code @done (18-09-25 15:39) @project(TODO)
|
||||
✔ Ability to include/exclude vods from shuffle in config @done (18-09-25 15:39) @project(TODO)
|
||||
✔ Add random chance for room grind playlist to show for certain amount of time @done (18-09-21 12:28) @project(TODO)
|
||||
✔ show commercials after a video length cap is hit -- show at conclusion of video @done (18-09-19 11:11) @project(TODO)
|
||||
✔ add memes to commercial scene @done (18-09-19 11:11) @project(TODO)
|
||||
✔ add $setcurrent support (to update text label through obs websocket instead of chat) @done (18-09-17 18:06) @project(TODO)
|
||||
✔ remember the last X vids played, remove these from shuffle choices @done (18-09-17 14:34) @project(TODO)
|
||||
730
fgfm.js
730
fgfm.js
@@ -1,730 +0,0 @@
|
||||
/**
|
||||
* FG.fm Automation
|
||||
*/
|
||||
|
||||
// Import 3rd party packages
|
||||
const irc = require('irc');
|
||||
const schedule = require('node-schedule');
|
||||
const md5 = require('md5');
|
||||
const moment = require('moment');
|
||||
|
||||
// Import local packages
|
||||
const GHOBS = require('./lib/ghobs');
|
||||
const FGFM = require('./lib/fgfm');
|
||||
const cooldowns = require('./lib/cooldowns');
|
||||
const util = require('./lib/util');
|
||||
const Spotify = require('./lib/spotify');
|
||||
|
||||
// Read internal configuration
|
||||
let config = require('./config.json');
|
||||
config.vods = require(config.vodConfigFile);
|
||||
config.rooms = require(config.roomConfigFile);
|
||||
let snesGames = require('./conf/snesgames.json');
|
||||
let timersList = require('./conf/timers.json');
|
||||
|
||||
let activeTimers = [];
|
||||
let skipVote = {target: null, count: 0};
|
||||
|
||||
// Main screen turn on
|
||||
const obs = new GHOBS(config);
|
||||
obs.init()
|
||||
.then(() => twitchInit(config.twitch))
|
||||
.then(twitch => streamInit(config, twitch))
|
||||
.catch(console.error);
|
||||
|
||||
// Connect to twitch, set up basic event listeners
|
||||
const twitchInit = (config) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let controlRoom = `#chatrooms:${config.channelId}:${config.controlRoomId}`;
|
||||
|
||||
console.log(`Connecting to Twitch / ${config.channel} / ${controlRoom}`);
|
||||
|
||||
let defaultTwitchConfig = {
|
||||
autoRejoin: true,
|
||||
retryCount: 10,
|
||||
channels: [config.channel, controlRoom],
|
||||
debug: config.debug
|
||||
};
|
||||
|
||||
// Connect to Twitch with the bot account
|
||||
let botChat = new irc.Client(
|
||||
config.ircServer,
|
||||
config.botLogin.username,
|
||||
Object.assign({password: config.botLogin.oauth}, defaultTwitchConfig)
|
||||
);
|
||||
|
||||
// Connect to Twitch with an editor account
|
||||
let editorChat = new irc.Client(
|
||||
config.ircServer,
|
||||
config.editorLogin.username,
|
||||
Object.assign({password: config.editorLogin.oauth}, defaultTwitchConfig)
|
||||
);
|
||||
|
||||
let twitchErrorHandler = message => {
|
||||
if (message.command != 'err_unknowncommand') {
|
||||
console.error('Error from Twitch IRC Server: ', message);
|
||||
}
|
||||
};
|
||||
|
||||
// Set up bare minimum event listeners for Twitch
|
||||
botChat.addListener('error', twitchErrorHandler);
|
||||
editorChat.addListener('error', twitchErrorHandler);
|
||||
|
||||
resolve({"botChat": botChat, "editorChat": editorChat, "controlRoom": controlRoom});
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize Stream automation
|
||||
const streamInit = (config, twitch) => {
|
||||
|
||||
// All your comfy are belong to us
|
||||
const director = new FGFM({config: config, obs: obs});
|
||||
|
||||
// Handle show events from the director
|
||||
director.on('SHOW_STARTED', () => {
|
||||
manageTimer('vr', 'on');
|
||||
});
|
||||
director.on('SHOW_PAUSED', () => {
|
||||
manageTimer('vr', 'off');
|
||||
});
|
||||
director.on('SHOW_RESUMED', () => {
|
||||
manageTimer('vr', 'on');
|
||||
});
|
||||
|
||||
director.on('SHOW_ENDING', (secondsUntilCredits) => {
|
||||
manageTimer('vr', 'off');
|
||||
|
||||
// Let the chat know the stream is ending soon
|
||||
twitch.botChat.say(config.twitch.channel, `The stream will be ending in ${parseFloat(secondsUntilCredits/60).toFixed(0)} minutes!`);
|
||||
});
|
||||
|
||||
director.on('CREDITS_SHOWN', (secondsUntilEnd) => {
|
||||
twitch.editorChat.say(config.twitch.channel, `Thanks to everyone for watching and lurking! Have a wonderful night and stay comfy. greenhComfy`);
|
||||
});
|
||||
|
||||
// Spotify integration
|
||||
const spotify = new Spotify(config.spotify);
|
||||
spotify.init();
|
||||
|
||||
// Chat commands
|
||||
const commands = {
|
||||
admin: {
|
||||
|
||||
init: (cmd) => {
|
||||
let streamStartDelaySeconds = cmd.args[1] || 1;
|
||||
let showStartDelaySeconds = cmd.args[2] || 300;
|
||||
|
||||
director.startingSoon(streamStartDelaySeconds, showStartDelaySeconds);
|
||||
},
|
||||
|
||||
|
||||
start: (cmd) => {
|
||||
director.startTheShow();
|
||||
},
|
||||
|
||||
|
||||
end: (cmd) => {
|
||||
let creditsDelay = cmd.args[1] || 1;
|
||||
let endDelay = cmd.args[2] || 60;
|
||||
director.endTheShow(creditsDelay, endDelay);
|
||||
},
|
||||
|
||||
|
||||
changevis: (cmd, newVisibility) => {
|
||||
let sceneItem = command.args[1] || false;
|
||||
if (!sceneItem) {
|
||||
twitch.botChat.say(cmd.to, `A scene item name is required!`);
|
||||
return;
|
||||
}
|
||||
|
||||
let sceneOrGroup = command.args[2] || obs.currentScene;
|
||||
obs.setVisible(sceneItem, sceneOrGroup, newVisibility).catch(console.error);
|
||||
},
|
||||
|
||||
|
||||
show: (cmd) => {
|
||||
commands.admin.changevis(cmd, true);
|
||||
},
|
||||
|
||||
|
||||
hide: (cmd) => {
|
||||
commands.admin.changevis(cmd, false);
|
||||
},
|
||||
|
||||
|
||||
t: (cmd) => {
|
||||
let sceneItem = cmd.args[1] || false;
|
||||
if (!sceneItem) {
|
||||
twitch.botChat.say(cmd.to, `A scene item name is required!`);
|
||||
return;
|
||||
}
|
||||
|
||||
obs.toggleVisible(sceneItem).catch(console.error);
|
||||
},
|
||||
|
||||
|
||||
timer: (cmd) => {
|
||||
let timerName = cmd.args[1] || false;
|
||||
if (!timerName) {
|
||||
twitch.botChat.say(cmd.to, `A timer name is required!`);
|
||||
return;
|
||||
}
|
||||
|
||||
let timerStatus = cmd.args[2] || false;
|
||||
|
||||
try {
|
||||
manageTimer(timerName, timerStatus);
|
||||
} catch (e) {
|
||||
twitch.botChat.say(cmd.to, e);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
auw: (cmd) => {
|
||||
director.showMeme('auw');
|
||||
},
|
||||
|
||||
|
||||
meme: (cmd) => {
|
||||
let memeId = cmd.args[1] || false;
|
||||
if (memeId) {
|
||||
console.log(`${memeId} meme requested by ${cmd.from}`);
|
||||
if ( config.vods.memes.findIndex(e => e.id === memeId) === -1) {
|
||||
twitch.botChat.say(cmd.to, `No meme with that ID exists!`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
memeId = config.vods.memes.sort(util.randSort)[0].id;
|
||||
console.log(`${memeId} meme randomly selected`);
|
||||
}
|
||||
|
||||
director.showMeme(memeId);
|
||||
},
|
||||
|
||||
|
||||
switch: (cmd) => {
|
||||
let newScene = cmd.args[1] || false;
|
||||
if (!newScene) {
|
||||
twitch.botChat.say(cmd.to, `A scene name is required!`);
|
||||
return;
|
||||
}
|
||||
|
||||
obs.switchToScene(newScene).catch(console.error);
|
||||
},
|
||||
|
||||
|
||||
setact: (cmd) => {
|
||||
let newActivity = cmd.args.slice(1).join(' ');
|
||||
if (!newActivity) {
|
||||
twitch.botChat.say(cmd.to, `Please provide a new activity`);
|
||||
return;
|
||||
}
|
||||
|
||||
obs.showActivity(newActivity).catch(console.error);
|
||||
},
|
||||
|
||||
|
||||
showact: (cmd) => {
|
||||
obs.showActivity().catch(console.error);
|
||||
},
|
||||
|
||||
|
||||
hideact: (cmd) => {
|
||||
obs.hideActivity().catch(console.error);
|
||||
},
|
||||
|
||||
|
||||
add: (cmd) => {
|
||||
// @TODO: DRY this out with the checks in vr
|
||||
let requestedVideoId = cmd.args[1] || false;
|
||||
if (requestedVideoId === false) {
|
||||
twitch.botChat.say(cmd.to, `Missing video ID`);
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure request vid isn't in the queue already
|
||||
// @TODO: Move into FGFM
|
||||
if (director.state.videoQueue.findIndex(e => e.id == requestedVideoId) !== -1) {
|
||||
twitch.botChat.say(cmd.to, `That video is in the queue already!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// search for req'd vid by id in config.vods.alttp
|
||||
let vodIndex = config.vods.alttp.findIndex(e => e.id == requestedVideoId);
|
||||
if (vodIndex === -1) {
|
||||
twitch.botChat.say(cmd.to, `A video with that ID does not exist!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// add to queue if it exists
|
||||
// @TODO: Move into FGFM
|
||||
if (director.addVideo(config.vods.alttp[vodIndex])) {
|
||||
twitch.botChat.say(cmd.to, `${config.vods.alttp[vodIndex].chatName} has been added to the queue [${director.state.videoQueue.length}]`);
|
||||
} else {
|
||||
twitch.botChat.say(cmd.to, `Video could not be added to queue!`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
skip: (cmd) => {
|
||||
director.skip();
|
||||
},
|
||||
|
||||
|
||||
pause: (cmd) => {
|
||||
director.pause();
|
||||
},
|
||||
|
||||
|
||||
resume: (cmd) => {
|
||||
director.resume();
|
||||
},
|
||||
|
||||
|
||||
clear: (cmd) => {
|
||||
director.clearQueue();
|
||||
},
|
||||
|
||||
|
||||
startvote: (cmd) => {
|
||||
videoVoteJob.reschedule(`*/${config.videoPollIntervalMinutes} * * * *`);
|
||||
twitch.botChat.say(cmd.to, `Video Queue Voting will start in ${config.videoPollIntervalMinutes} minutes!`);
|
||||
},
|
||||
|
||||
|
||||
pausevote: (cmd) => {
|
||||
clearInterval(rtvInterval);
|
||||
videoVoteJob.cancel();
|
||||
twitch.botChat.say(cmd.to, `Video Queue Voting has been paused.`);
|
||||
},
|
||||
|
||||
|
||||
songskip: (cmd) => {
|
||||
spotify.skip();
|
||||
},
|
||||
|
||||
songpause: (cmd) => {
|
||||
spotify.pause();
|
||||
},
|
||||
|
||||
songresume: (cmd) => {
|
||||
spotify.resume();
|
||||
},
|
||||
|
||||
songvol: (cmd) => {
|
||||
let volume = parseInt(cmd.args[1]) || 100;
|
||||
spotify.setVolume(volume)
|
||||
.then(res => twitch.botChat.say(cmd.to, `Volume set to ${volume}`))
|
||||
.catch(err => twitch.botChat.say(cmd.to, `Error setting spotify volume: ${JSON.stringify(err)}`));
|
||||
},
|
||||
|
||||
songplay: (cmd) => {
|
||||
let url = cmd.args[1] || false;
|
||||
if (url === false) {
|
||||
return twitch.botChat.say(cmd.to, `You must provide a link to a spotify playlist or album!`);
|
||||
}
|
||||
|
||||
// parse+validate url
|
||||
let spotifyUri = false;
|
||||
|
||||
// check for native spotify URI first
|
||||
if (url.includes('spotify:')) {
|
||||
let parsedUrl = url.match(/spotify:(playlist|album):([A-Za-z0-9]{22})/);
|
||||
if (parsedUrl !== null) {
|
||||
spotifyUri = parsedUrl[0];
|
||||
}
|
||||
} else if (url.includes('spotify.com')) {
|
||||
// determine if it's an album or playlist
|
||||
if (!url.includes('/playlist/') && !url.includes('/album/')) {
|
||||
return twitch.botChat.say(cmd.to, `Spotify URL must be a playlist or album!`);
|
||||
}
|
||||
|
||||
// parse the URL to get the resource type and ID
|
||||
let parsedUrl = url.match(/(playlist|album)\/([A-Za-z0-9]{22})/);
|
||||
if (parsedUrl !== null) {
|
||||
spotifyUri = `spotify:${parsedUrl[1]}:${parsedUrl[2]}`;
|
||||
} else {
|
||||
return twitch.botChat.say(cmd.to, `Unable to parse spotify URL!`);
|
||||
}
|
||||
} else {
|
||||
return twitch.botChat.say(cmd.to, `Invalid spotify URL!`);
|
||||
}
|
||||
|
||||
if (spotifyUri !== false) {
|
||||
spotify.playContext(spotifyUri)
|
||||
.then(res => twitch.botChat.say(cmd.to, `Changed playlist!`))
|
||||
.catch(err => twitch.botChat.say(cmd.to, `Error changing playlist: ${JSON.stringify(err)}`));
|
||||
} else {
|
||||
return twitch.botChat.say(cmd.to, `Unable to parse Spotify URL!`);
|
||||
}
|
||||
},
|
||||
|
||||
songshuffle: (cmd) => {
|
||||
let state = cmd.args[1] || true;
|
||||
|
||||
if (state === 'off' || state === 'false') {
|
||||
state = false;
|
||||
} else {
|
||||
state = true;
|
||||
}
|
||||
|
||||
spotify.shuffle(state)
|
||||
.then(res => twitch.botChat.say(cmd.to, `Updated shuffle state!`))
|
||||
.catch(err => twitch.botChat.say(cmd.to, `Error changing shuffle state: ${JSON.stringify(err)}`))
|
||||
},
|
||||
|
||||
songrepeat: (cmd) => {
|
||||
let state = cmd.args[1] || false;
|
||||
if (state === false) {
|
||||
return twitch.botChat.say(cmd.to, `You must provide a repeat mode (track, context, or off)!`);
|
||||
}
|
||||
|
||||
if (!['track', 'context', 'off'].includes(state)) {
|
||||
return twitch.botChat.say(cmd.to, `You must provide a valid repeat mode (track, context, or off)!`);
|
||||
}
|
||||
|
||||
spotify.repeat(state)
|
||||
.then(res => twitch.botChat.say(cmd.to, `Updated repeat mode!`))
|
||||
.catch(err => twitch.botChat.say(cmd.to, `Error changing repeat mode: ${JSON.stringify(err)}`))
|
||||
},
|
||||
|
||||
reboot: (cmd) => {
|
||||
console.log('Received request from admin to reboot...');
|
||||
twitch.botChat.say(cmd.to, 'Rebooting...');
|
||||
process.exit(0); // requires process manager with autoreboot to work
|
||||
}
|
||||
},
|
||||
|
||||
user: {
|
||||
|
||||
vote: (cmd) => {
|
||||
let userVote = cmd.args[1] || false;
|
||||
|
||||
if (userVote === false) {
|
||||
rockTheVote();
|
||||
return;
|
||||
}
|
||||
|
||||
userVote = Number.parseInt(userVote);
|
||||
|
||||
if (!Number.isInteger(userVote) || userVote < 1 || userVote > currentChoices.length) {
|
||||
return twitch.botChat.say(cmd.to, `@${from}, please choose an option from 1 - ${currentChoices.length}!`);
|
||||
}
|
||||
|
||||
// Check for uniqueness of vote
|
||||
// if it's not unique, update the vote
|
||||
let prevVote = userVotes.findIndex(e => e.from === from);
|
||||
if (prevVote !== -1) {
|
||||
if (userVotes[prevVote].vote !== userVote) {
|
||||
// update vote and inform the user
|
||||
userVotes[prevVote].vote = userVote;
|
||||
twitch.botChat.say(cmd.to, `@${from}, your vote has been updated!`);
|
||||
} else {
|
||||
twitch.botChat.say(cmd.to, `@${from}, your vote is already in!`);
|
||||
}
|
||||
} else {
|
||||
// log user vote
|
||||
userVotes.push({"from": from, "vote": userVote});
|
||||
twitch.botChat.say(cmd.to, `@${from}, your vote has been logged!`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
queue: (cmd) => {
|
||||
// @TODO: Move into FGFM
|
||||
if (director.state.videoQueue.length > 0) {
|
||||
let chatQueue = director.state.videoQueue.slice(0, 10).map((c, i) => {
|
||||
return `[${i+1}] ${c.chatName}`;
|
||||
});
|
||||
twitch.botChat.say(cmd.to, chatQueue.join(' | '));
|
||||
} else {
|
||||
twitch.botChat.say(cmd.to, `No videos currently in queue!`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
current: (cmd) => {
|
||||
// @TODO: Move retrieval of currentVideo into FGFM
|
||||
twitch.botChat.say(cmd.to, `Now Playing: ${director.state.currentVideo.chatName}`);
|
||||
},
|
||||
|
||||
|
||||
next: (cmd) => {
|
||||
// @TODO: Move retrieval of videoQueue into FGFM
|
||||
if (director.state.videoQueue.length > 0) {
|
||||
twitch.botChat.say(cmd.to, `Next Video: ${director.state.videoQueue[0].chatName}`);
|
||||
} else {
|
||||
twitch.botChat.say(cmd.to, `No videos currently in queue!`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
vr: (cmd) => {
|
||||
let requestedVideoId = cmd.args[1] || false;
|
||||
if (requestedVideoId === false) {
|
||||
twitch.botChat.say(cmd.to, `Useage: ${config.twitch.cmdPrefix}vr <video-id> | Videos: https://pastebin.com/qv0wDkvB`);
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure request vid isn't in the queue already
|
||||
// @TODO: Move check into FGFM
|
||||
if (director.state.videoQueue.findIndex(e => e.id === requestedVideoId) !== -1) {
|
||||
twitch.botChat.say(cmd.to, `That video is in the queue already!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// search for req'd vid by id in config.vods.alttp
|
||||
let vodIndex = config.vods.alttp.findIndex(e => e.id === requestedVideoId);
|
||||
if (vodIndex === -1) {
|
||||
twitch.botChat.say(cmd.to, `A video with that ID does not exist!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: Make sure user hasn't met the request limit
|
||||
|
||||
config.vods.alttp[vodIndex].requestedBy = cmd.from;
|
||||
|
||||
// add to queue if it exists
|
||||
// @TODO: Return queue position from addVideo
|
||||
if (director.addVideo(config.vods.alttp[vodIndex])) {
|
||||
twitch.botChat.say(cmd.to, `${config.vods.alttp[vodIndex].chatName} has been added to the queue [${director.state.videoQueue.length}]`);
|
||||
} else {
|
||||
twitch.botChat.say(cmd.to, `${config.vods.alttp[vodIndex].chatName} could not be added to the queue!`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
room: (cmd) => {
|
||||
let roomId = cmd.args[1] || false;
|
||||
let room;
|
||||
|
||||
if (roomId !== false) {
|
||||
let roomIndex = config.rooms.findIndex(e => e.id === parseInt(roomId));
|
||||
|
||||
if (roomIndex === -1) {
|
||||
twitch.botChat.say(cmd.to, `No room found matching that ID!`);
|
||||
return;
|
||||
}
|
||||
|
||||
room = config.rooms[roomIndex];
|
||||
} else {
|
||||
twitch.botChat.say(cmd.to, `Useage: ${config.twitch.cmdPrefix}room <room-id> | Rooms: https://goo.gl/qoNmuH`);
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: Make sure user hasn't met the request limit
|
||||
|
||||
room.requestedBy = cmd.from;
|
||||
|
||||
director.addRoomVideo(room);
|
||||
// @TODO: Return new queue position from addRoomVideo and use below
|
||||
twitch.botChat.say(cmd.to, `Added ${room.dungeonName||'?'} - ${room.roomName||'?'} to the queue [${director.state.videoQueue.length}]!`);
|
||||
},
|
||||
|
||||
|
||||
rngames: (cmd) => {
|
||||
twitch.botChat.say(cmd.to, snesGames.sort(util.randSort).slice(0, 10).join(' | '));
|
||||
},
|
||||
|
||||
// voting to skip current video
|
||||
skip: (cmd) => {
|
||||
// check if there is an existing vote to skip for the director.state.currentVideo
|
||||
if (skipVote.target === director.state.currentVideo.id) {
|
||||
// if yes, add the vote, check if threshold is met, skip if necessary
|
||||
skipVote.count++;
|
||||
} else {
|
||||
skipVote.target = director.state.currentVideo.id;
|
||||
skipVote.count = 1;
|
||||
}
|
||||
|
||||
if (skipVote.count >= config.skipVoteThreshold) {
|
||||
director.skip();
|
||||
skipVote.target = null;
|
||||
}
|
||||
},
|
||||
|
||||
song: async (cmd) => {
|
||||
spotify.getCurrentSong()
|
||||
.then(async song => {
|
||||
let artists = [];
|
||||
await util.asyncForEach(song.artists, async (artist) => artists.push(artist.name));
|
||||
twitch.botChat.say(cmd.to, `Current Song: ${artists.join(',')} - ${song.name} | ${song.url}`);
|
||||
})
|
||||
.catch(err => twitch.botChat.say(cmd.to, `Error retrieving current song: ${JSON.stringify(err)}`));
|
||||
},
|
||||
|
||||
playlist: (cmd) => {
|
||||
spotify.getCurrentPlaylist()
|
||||
.then(playlist => twitch.botChat.say(cmd.to, `Current Playlist: ${playlist}`))
|
||||
.catch(err => twitch.botChat.say(cmd.to, `Error retrieving current playlist: ${JSON.stringify(err)}`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Aliases for chat commands
|
||||
const aliases = {
|
||||
"rooms": "room"
|
||||
};
|
||||
|
||||
// Listen for the above commands
|
||||
twitch.botChat.addListener('message', (from, to, message) => {
|
||||
// Ignore everything from blacklisted users
|
||||
if (config.twitch.blacklistedUsers.includes(from)) return;
|
||||
|
||||
// Ignore commands that don't start with the designated prefix
|
||||
if (!message.startsWith(config.twitch.cmdPrefix)) return;
|
||||
|
||||
// Remove command prefix for parsing
|
||||
let noPrefix = message.slice(config.twitch.cmdPrefix.length);
|
||||
|
||||
// Ignore blank commands
|
||||
if (noPrefix.length === 0) return;
|
||||
|
||||
// Parse command arguments
|
||||
let args = noPrefix.split(' ');
|
||||
let key = args[0] || '';
|
||||
|
||||
// Ignore messages without a command
|
||||
if (!key || key.length === 0) return;
|
||||
|
||||
// Case-insensitive
|
||||
key.toLowerCase();
|
||||
|
||||
// Check for aliased commands
|
||||
if (aliases.hasOwnProperty(key)) key = aliases[key];
|
||||
|
||||
// Ignore unrecognized commands
|
||||
if (!commands.admin.hasOwnProperty(key) && !commands.user.hasOwnProperty(key)) return;
|
||||
|
||||
// Check if the command is on cooldown for this user in this channel (admins bypass this)
|
||||
let cooldownKey = md5(from+to+key);
|
||||
cooldowns.get(cooldownKey, config.twitch.defaultUserCooldown)
|
||||
.then(onCooldown => {
|
||||
if (onCooldown === false || config.twitch.admins.includes(from)) {
|
||||
let command = {message: message, from: from, to: to, key: key, args: args};
|
||||
|
||||
// Handle admin commands
|
||||
if (commands.admin.hasOwnProperty(command.key) && config.twitch.admins.includes(from)) {
|
||||
return commands.admin[command.key](command);
|
||||
}
|
||||
|
||||
// Handle all other user commands
|
||||
if (commands.user.hasOwnProperty(command.key)) {
|
||||
// Place this command on cooldown for the user
|
||||
cooldowns.set(cooldownKey, config.twitch.defaultUserCooldown);
|
||||
return commands.user[command.key](command);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
const manageTimer = (timerName, timerStatus) => {
|
||||
// search timers for matching name
|
||||
let theTimerIndex = timersList.findIndex(e => e.name === timerName);
|
||||
if (theTimerIndex === -1) {
|
||||
throw("Invalid timer name!");
|
||||
}
|
||||
|
||||
let theTimer = timersList[theTimerIndex];
|
||||
|
||||
// look in activeTimers for current status
|
||||
let currentTimerIndex = activeTimers.findIndex(e => e.name === timerName);
|
||||
|
||||
if (!timerStatus || timerStatus !== 'on' || timerStatus !== 'off') {
|
||||
// toggle by default
|
||||
if (currentTimerIndex === -1) {
|
||||
timerStatus = 'on';
|
||||
} else {
|
||||
timerStatus = 'off';
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTimerIndex === -1 && timerStatus === 'on') {
|
||||
let timerFunc = () => {
|
||||
twitch.botChat.say(config.twitch.channel, theTimer.value);
|
||||
};
|
||||
let timerInterval = setInterval(timerFunc, theTimer.interval*1000);
|
||||
activeTimers.push({name: theTimer.name, timer: timerInterval});
|
||||
timerFunc();
|
||||
} else if (timerStatus === 'off') {
|
||||
clearInterval(activeTimers[currentTimerIndex].timer);
|
||||
activeTimers.splice(currentTimerIndex, 1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: Modularize timed events
|
||||
//console.log(`Initializing stream timers...`);
|
||||
let userVotes = currentChoices = [];
|
||||
let rockTheVote = () => {};
|
||||
// @TODO: Move this interval to config
|
||||
let rtvInterval = setInterval(() => {rockTheVote()}, 300000);
|
||||
let videoVoteJob = new schedule.Job(async () => {
|
||||
// Tally votes from previous election (if there was one), add the winner to the queue
|
||||
let winner;
|
||||
if (currentChoices.length > 0) {
|
||||
if (userVotes.length === 0) {
|
||||
// choose a random element from currentChoices
|
||||
winner = util.randElement(currentChoices);
|
||||
console.log(`VIDEO CHOSEN RANDOMLY: ${winner.chatName}`);
|
||||
twitch.botChat.say(config.twitch.channel, `No Votes Logged -- Next Video Chosen at Random: ${winner.chatName}`);
|
||||
} else {
|
||||
// tally and sort votes
|
||||
let voteTallies = [];
|
||||
await util.asyncForEach(userVotes, async (vote) => {
|
||||
tallyIndex = voteTallies.findIndex(e => e.id === vote.vote);
|
||||
if (tallyIndex !== -1) {
|
||||
voteTallies[tallyIndex].count++;
|
||||
} else {
|
||||
voteTallies.push({id: vote.vote, count: 1});
|
||||
}
|
||||
});
|
||||
voteTallies.sort((a, b) => {
|
||||
if (a.count < b.count) {
|
||||
return -1;
|
||||
}
|
||||
if (a.count > b.count) {
|
||||
return 1;
|
||||
}
|
||||
// a must be equal to b
|
||||
return 0;
|
||||
});
|
||||
|
||||
console.log(`Voting Results: ${JSON.stringify(voteTallies)}`);
|
||||
winner = currentChoices[voteTallies[0].id-1];
|
||||
console.log(`WINNER OF THE VOTE: ${winner.chatName}`);
|
||||
twitch.botChat.say(config.twitch.channel, `Winner of the Video Vote: ${winner.chatName}`);
|
||||
|
||||
// clear user votes
|
||||
userVotes = [];
|
||||
}
|
||||
|
||||
director.addVideo(winner);
|
||||
}
|
||||
|
||||
// choose more random videos from config.vods.alttp (that aren't already in the queue)
|
||||
// @TODO: Move into FGFM
|
||||
let vodsNotInQueue = config.vods.alttp.filter(e => {
|
||||
let inQueue = (director.state.videoQueue.findIndex(q => q.id === e.id) !== -1) && (director.state.currentVideo.id !== e.id);
|
||||
return !inQueue;
|
||||
});
|
||||
currentChoices = vodsNotInQueue.sort(util.randSort).slice(0, config.videoPollSize);
|
||||
|
||||
// Poll the chat
|
||||
let chatChoices = currentChoices.map((c, i) => {
|
||||
return `[${i+1}] ${c.chatName}`;
|
||||
});
|
||||
|
||||
rockTheVote = () => {
|
||||
twitch.botChat.say(config.twitch.channel, `Vote for which video you'd like to add to the queue using ${config.twitch.cmdPrefix}vote #: ${chatChoices.join(' | ')}`)
|
||||
};
|
||||
clearInterval(rtvInterval);
|
||||
rockTheVote();
|
||||
rtvInterval = setInterval(() => {rockTheVote()}, 300000);
|
||||
});
|
||||
};
|
||||
|
||||
// catches Promise errors
|
||||
process.on('unhandledRejection', console.error);
|
||||
547
legacy.js
Normal file
547
legacy.js
Normal file
@@ -0,0 +1,547 @@
|
||||
// Import modules
|
||||
const Discord = require("discord.js"),
|
||||
fs = require("fs"),
|
||||
path = require("path"),
|
||||
axios = require("axios"),
|
||||
schedule = require("node-schedule"),
|
||||
staticCommands = require("./lib/static-commands.js"),
|
||||
ankhbotCommands = require("./lib/ankhbot-commands.js"),
|
||||
{ randElement, chunkSubstr } = require("./lib/utils.js"),
|
||||
config = require("./config.json");
|
||||
|
||||
function init(config) {
|
||||
// Set up Discord client
|
||||
const client = new Discord.Client();
|
||||
|
||||
// Set up SFX
|
||||
const sfxFilePath = path.join(__dirname, "sfx");
|
||||
|
||||
// Read in sfx directory, filenames are the commands
|
||||
let sfxList = readSfxDirectory(sfxFilePath);
|
||||
// Watch directory for changes and update the list
|
||||
fs.watch(sfxFilePath, (eventType, filename) => {
|
||||
if (eventType === "rename") {
|
||||
sfxList = readSfxDirectory(sfxFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
// @todo DRY this shit up
|
||||
|
||||
// Read in fun facts
|
||||
const funFactsFilePath = path.join(__dirname, "conf", "funfacts");
|
||||
let funFacts = parseLines(funFactsFilePath);
|
||||
fs.watchFile(funFactsFilePath, (curr, prev) => {
|
||||
if (curr.mtime !== prev.mtime) {
|
||||
funFacts = parseLines(funFactsFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
// Read in ham facts
|
||||
const hamFactsFilePath = path.join(__dirname, "conf", "hamfacts");
|
||||
let hamFacts = parseLines(hamFactsFilePath);
|
||||
fs.watchFile(hamFactsFilePath, (curr, prev) => {
|
||||
if (curr.mtime !== prev.mtime) {
|
||||
hamFacts = parseLines(hamFactsFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up the native commands to handle
|
||||
const commands = {
|
||||
sfx: async (msg, guildConfig) => {
|
||||
let allowedSfxChannels = new RegExp(guildConfig.allowedSfxChannels);
|
||||
if (!allowedSfxChannels.test(msg.channel.name)) return;
|
||||
let sfx = msg.content.split(" ")[1];
|
||||
|
||||
// retrieve sfx list from pastebin
|
||||
if (sfx == "" || sfx === undefined) {
|
||||
axios
|
||||
.get("https://rentry.co/ghbotsfx/raw")
|
||||
.then((res) => {
|
||||
// break the result into half chunks if it exceeds the message limit size
|
||||
// (discord limit is 2k)
|
||||
let chunks = [res.data];
|
||||
if (res.data.length > 2000) {
|
||||
chunks = chunkSubstr(res.data, res.data.length / 2);
|
||||
}
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
return msg.channel.send(chunk);
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// make sure this file exists either as an mp3 or wav
|
||||
let sfxPath;
|
||||
if (fs.existsSync(path.join(sfxFilePath, sfx + ".mp3"))) {
|
||||
sfxPath = path.join(sfxFilePath, sfx + ".mp3");
|
||||
} else if (fs.existsSync(path.join(sfxFilePath, sfx + ".wav"))) {
|
||||
sfxPath = path.join(sfxFilePath, sfx + ".wav");
|
||||
} else {
|
||||
return msg.reply("This sound effect does not exist!");
|
||||
}
|
||||
|
||||
// Join the same voice channel of the author of the message
|
||||
const connection = await joinVoiceChannel(msg);
|
||||
if (connection === false) {
|
||||
return msg.reply("I couldn't connect to your voice channel...");
|
||||
}
|
||||
|
||||
(function play(sfxFile) {
|
||||
const dispatcher = connection.play(sfxFile, {
|
||||
volume: guildConfig.sfxVolume,
|
||||
passes: guildConfig.passes,
|
||||
});
|
||||
dispatcher
|
||||
.on("finish", (reason) => {
|
||||
connection.disconnect();
|
||||
})
|
||||
.on("error", (error) => {
|
||||
connection.disconnect();
|
||||
console.error("Error playing sfx: " + error);
|
||||
})
|
||||
.on("start", () => {});
|
||||
})(sfxPath.toString());
|
||||
},
|
||||
funfact: (msg, guildConfig) => {
|
||||
if (guildConfig.enableFunFacts === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (funFacts.length > 0) {
|
||||
// return random element from funFacts, unless one is specifically requested
|
||||
let el;
|
||||
let req = parseInt(msg.content.split(" ")[1]);
|
||||
if (Number.isNaN(req) || typeof funFacts[req - 1] === "undefined") {
|
||||
el = Math.floor(Math.random() * funFacts.length);
|
||||
} else {
|
||||
el = req - 1;
|
||||
}
|
||||
|
||||
let displayNum = (el + 1).toString();
|
||||
let funFact = funFacts[el];
|
||||
msg.channel
|
||||
.send({
|
||||
embed: {
|
||||
title: "FunFact #" + displayNum,
|
||||
color: 0x21c629,
|
||||
description: funFact,
|
||||
},
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
msg.channel.send("No fun facts found!");
|
||||
}
|
||||
},
|
||||
hamfact: (msg, guildConfig) => {
|
||||
if (guildConfig.enableHamFacts === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hamFacts.length > 0) {
|
||||
// return random element from hamFacts, unless one is specifically requested
|
||||
let el;
|
||||
let req = parseInt(msg.content.split(" ")[1]);
|
||||
if (Number.isNaN(req) || typeof hamFacts[req - 1] === "undefined") {
|
||||
el = Math.floor(Math.random() * hamFacts.length);
|
||||
} else {
|
||||
el = req - 1;
|
||||
}
|
||||
|
||||
let displayNum = (el + 1).toString();
|
||||
let hamFact = hamFacts[el];
|
||||
msg.channel
|
||||
.send({
|
||||
embed: {
|
||||
title: "HamFact #" + displayNum,
|
||||
color: 0x21c629,
|
||||
description: hamFact,
|
||||
},
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
msg.channel.send("No ham facts found!");
|
||||
}
|
||||
},
|
||||
dance: (msg, guildConfig) => {
|
||||
msg.channel.send(
|
||||
"*┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛*"
|
||||
);
|
||||
},
|
||||
// Allow members to request role additions/removals for allowed roles
|
||||
role: (msg, guildConfig) => {
|
||||
// make sure there are allowed roles defined
|
||||
if (
|
||||
typeof guildConfig.allowedRolesForRequest === undefined ||
|
||||
guildConfig.allowedRolesForRequest.length === 0
|
||||
) {
|
||||
return msg.reply(
|
||||
"No roles are currently allowed to be added/removed by members."
|
||||
);
|
||||
}
|
||||
|
||||
let validRoles = guildConfig.allowedRolesForRequest.split("|");
|
||||
|
||||
if (msg.content === guildConfig.prefix + "role") {
|
||||
return msg.reply(
|
||||
`Useage: ${guildConfig.prefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}`
|
||||
);
|
||||
}
|
||||
|
||||
// parse+validate action+role (use original case from message because roles are case-sensitive)
|
||||
let roleName = msg.originalContent.match(
|
||||
/role\s(add|remove)\s([a-z0-9\-]+)/i
|
||||
);
|
||||
if (!roleName) {
|
||||
return msg.reply(
|
||||
`Useage: ${guildConfig.prefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}`
|
||||
);
|
||||
} else {
|
||||
let tester = new RegExp(guildConfig.allowedRolesForRequest, "i");
|
||||
if (tester.test(roleName[2])) {
|
||||
// make sure this message is in a guild channel they're a member of
|
||||
if (!msg.guild) return;
|
||||
|
||||
// find the role in the member's guild
|
||||
let role = msg.guild.roles.cache.find((x) => x.name === roleName[2]);
|
||||
|
||||
if (!role) {
|
||||
return msg.reply(`${roleName[2]} is not a role on this server!`);
|
||||
}
|
||||
|
||||
// add/remove the role and react to the message with the results
|
||||
if (roleName[1] === "add") {
|
||||
msg.member.roles
|
||||
.add(role, "User requested")
|
||||
.then((requestingMember) => {
|
||||
msg
|
||||
.react("👍")
|
||||
.then(() => {
|
||||
console.log("Reaction sent");
|
||||
})
|
||||
.catch(console.error);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error during role addition: ${err}`);
|
||||
msg
|
||||
.react("⚠")
|
||||
.then(() => {
|
||||
console.log("Reaction sent");
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
} else if (roleName[1] === "remove") {
|
||||
msg.member.roles
|
||||
.remove(role, "User requested")
|
||||
.then((requestingMember) => {
|
||||
msg
|
||||
.react("👍")
|
||||
.then(() => {
|
||||
console.log("Reaction sent");
|
||||
})
|
||||
.catch(console.error);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error during role addition: ${err}`);
|
||||
msg
|
||||
.react("⚠")
|
||||
.then(() => {
|
||||
console.log("Reaction sent");
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
} else {
|
||||
msg.reply(
|
||||
`You must use add/remove after the role command! *e.g. ${guildConfig.prefix}role add ${validRoles[0]}*`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
msg.reply(
|
||||
`**${
|
||||
roleName[2]
|
||||
}** is not a valid role name! The roles allowed for request are: ${validRoles.join(
|
||||
","
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
join: (msg, guildConfig) => {
|
||||
if (!msg.guild.voiceConnection) {
|
||||
joinVoiceChannel(msg)
|
||||
.then(() => {
|
||||
//
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
return msg.reply(`I'm already in a voice channel!`);
|
||||
}
|
||||
},
|
||||
leave: (msg, guildConfig) => {
|
||||
if (msg.guild.voiceConnection) {
|
||||
msg.guild.voiceConnection.disconnect();
|
||||
} else {
|
||||
return msg.reply(
|
||||
`If ya don't eat your meat, ya can't have any pudding!`
|
||||
);
|
||||
}
|
||||
},
|
||||
listen: (msg, guildConfig) => {
|
||||
// listen for a particular member to speak and respond appropriately
|
||||
if (msg.guild.voiceConnection) {
|
||||
// get the guild member
|
||||
//let guildMemberId = "88301001169207296"; // me
|
||||
let guildMemberId = "153563292265086977"; // Screevo
|
||||
let guildMember = msg.guild.members.get(guildMemberId);
|
||||
if (guildMember) {
|
||||
let listenInterval = 1000;
|
||||
setInterval(() => {
|
||||
if (guildMember.speaking === true) {
|
||||
msg.content = "!sfx stfu";
|
||||
commands.sfx(msg, false);
|
||||
}
|
||||
}, listenInterval);
|
||||
} else {
|
||||
console.error(
|
||||
`Could not find specified guild member: ${guildMemberId}!`
|
||||
);
|
||||
msg.guild.voiceConnection.disconnect();
|
||||
}
|
||||
} else {
|
||||
// join the voice channel then call this command again
|
||||
joinVoiceChannel(msg)
|
||||
.then(() => {
|
||||
commands.listen(msg, guildConfig);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
},
|
||||
reboot: (msg, guildConfig) => {
|
||||
if (msg.author.id == config.discord.adminUserId) process.exit(); // Requires a node module like Forever to work.
|
||||
},
|
||||
};
|
||||
|
||||
client
|
||||
// Wait for discord to be ready, handle messages
|
||||
.on("ready", () => {
|
||||
console.log(`${config.botName} is connected and ready`);
|
||||
client.setRandomActivity();
|
||||
setInterval(() => {
|
||||
client.setRandomActivity();
|
||||
}, 3600 * 1000);
|
||||
|
||||
// Set up scheduled events for each guild
|
||||
config.discord.guilds.forEach(async (guild) => {
|
||||
let discordGuild = false;
|
||||
try {
|
||||
discordGuild = await client.guilds.fetch(guild.id);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (!discordGuild) return;
|
||||
|
||||
if (
|
||||
guild.hasOwnProperty("scheduledEvents") &&
|
||||
guild.scheduledEvents.length > 0
|
||||
) {
|
||||
guild.scheduledEvents.forEach(async (event) => {
|
||||
let channel = false;
|
||||
if (
|
||||
event.hasOwnProperty("channelId") &&
|
||||
event.channelId.length > 0
|
||||
) {
|
||||
channel = await discordGuild.channels.resolve(event.channelId);
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
console.log(
|
||||
`Invalid channel configured for event ${event.id}, guild ${guild.name}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let pingRole = false;
|
||||
if (
|
||||
event.hasOwnProperty("pingRoleId") &&
|
||||
event.pingRoleId.length > 0
|
||||
) {
|
||||
pingRole = await discordGuild.roles.fetch(event.pingRoleId);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Scheduling event ${event.id} for ${discordGuild.name}...`
|
||||
);
|
||||
const job = schedule.scheduleJob(event.schedule, () => {
|
||||
let payload = [];
|
||||
if (pingRole !== false) {
|
||||
payload.push(pingRole);
|
||||
}
|
||||
if (event.hasOwnProperty("message") && event.message.length > 0) {
|
||||
payload.push(event.message);
|
||||
}
|
||||
channel.send(payload);
|
||||
});
|
||||
console.log(`Next invocation: ${job.nextInvocation()}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
// Listen for commands for the bot to respond to across all channels
|
||||
.on("message", (msg) => {
|
||||
// Ignore DMs and messages from unconfigured guilds
|
||||
if (msg.guild) {
|
||||
if (!config.discord.guilds.find((g) => g.id === msg.guild.id)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore anything from blacklisted users
|
||||
if (config.discord.blacklistedUsers.includes(msg.author.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the guild config for this msg, use default if no guild (DM)
|
||||
let guildConfig = config.discord.guilds.find(
|
||||
(g) => g.id === msg.guild.id
|
||||
);
|
||||
|
||||
// Parse message content
|
||||
msg.originalContent = msg.content;
|
||||
msg.content = msg.content.toLowerCase();
|
||||
|
||||
// Make sure the command starts with the configured prefix
|
||||
if (!msg.content.startsWith(guildConfig.prefix)) return;
|
||||
|
||||
let commandNoPrefix = msg.content
|
||||
.slice(guildConfig.prefix.length)
|
||||
.split(" ")[0];
|
||||
|
||||
console.log(
|
||||
`'${commandNoPrefix}' received in ${guildConfig.internalName}#${msg.channel.name} from @${msg.author.username}`
|
||||
);
|
||||
|
||||
// check for native command first
|
||||
if (commands.hasOwnProperty(commandNoPrefix)) {
|
||||
commands[commandNoPrefix](msg, guildConfig);
|
||||
// then a static command we've manually added
|
||||
} else if (staticCommands.exists(commandNoPrefix)) {
|
||||
let result = staticCommands.get(commandNoPrefix);
|
||||
msg.channel
|
||||
.send({
|
||||
embed: {
|
||||
title: commandNoPrefix,
|
||||
color: 0x21c629,
|
||||
description: result,
|
||||
},
|
||||
})
|
||||
.catch(console.error);
|
||||
// then a command exported from ankhbot
|
||||
} else if (ankhbotCommands.exists(commandNoPrefix)) {
|
||||
let result = ankhbotCommands.get(commandNoPrefix);
|
||||
msg.channel
|
||||
.send({
|
||||
embed: {
|
||||
title: commandNoPrefix,
|
||||
color: 0x21c629,
|
||||
description: result,
|
||||
},
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
// Not a command we recognize, ignore
|
||||
}
|
||||
})
|
||||
// Handle new members joining one of our guilds
|
||||
.on("guildMemberAdd", (member) => {
|
||||
// Ignore events from unconfigured guilds
|
||||
if (member.guild) {
|
||||
if (!config.discord.guilds.find((g) => g.id === msg.guild.id)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`A new member has joined '${member.guild.name}': ${member.displayName}`
|
||||
);
|
||||
})
|
||||
// Log guild becoming unavailable (usually due to server outage)
|
||||
.on("guildUnavailable", (guild) => {
|
||||
console.log(
|
||||
`Guild '${guild.name}' is no longer available! Most likely due to server outage.`
|
||||
);
|
||||
})
|
||||
// Log debug messages if enabled
|
||||
.on("debug", (info) => {
|
||||
if (config.debug === true) {
|
||||
console.log(`[${new Date()}] DEBUG: ${info}`);
|
||||
}
|
||||
})
|
||||
// Log disconnect event
|
||||
.on("disconnect", (event) => {
|
||||
console.log(
|
||||
`Web Socket disconnected with code ${event.code} and reason '${event.reason}'`
|
||||
);
|
||||
})
|
||||
// Log errors
|
||||
.on("error", console.error)
|
||||
// Log the bot in
|
||||
.login(config.discord.token);
|
||||
}
|
||||
|
||||
function readSfxDirectory(path) {
|
||||
let thePath = path || sfxFilePath;
|
||||
let sfxList = fs.readdirSync(thePath);
|
||||
sfxList.forEach(function (el, index, a) {
|
||||
a[index] = el.split(".")[0];
|
||||
});
|
||||
return sfxList;
|
||||
}
|
||||
|
||||
async function joinVoiceChannel(message) {
|
||||
// Join the same voice channel of the author of the message
|
||||
if (message.member.voice.channel) {
|
||||
return await message.member.voice.channel.join();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Read/parse text lines from a file
|
||||
function parseLines(filePath) {
|
||||
let lines = [];
|
||||
let data = fs.readFileSync(filePath, "utf-8");
|
||||
let splitLines = data.toString().split("\n");
|
||||
splitLines.forEach(function (line) {
|
||||
if (line.length > 0) {
|
||||
lines.push(line);
|
||||
}
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
// catch Promise errors
|
||||
process.on("unhandledRejection", console.error);
|
||||
|
||||
// Fire it up
|
||||
init(config);
|
||||
|
||||
Discord.Client.prototype.setRandomActivity = function () {
|
||||
let activity =
|
||||
config.discord.activities.length > 0
|
||||
? randElement(config.discord.activities)
|
||||
: "DESTROY ALL HUMANS";
|
||||
|
||||
console.log(`Setting Discord activity to: ${activity}`);
|
||||
|
||||
this.user.setActivity(activity, {
|
||||
url: `https://twitch.tv/fgfm`,
|
||||
type: "STREAMING",
|
||||
});
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
module.exports = {
|
||||
exists: exists,
|
||||
get: get
|
||||
};
|
||||
|
||||
const fs = require('fs'),
|
||||
path = require('path');
|
||||
|
||||
const cmdPrefix = '!';
|
||||
const commandsFilePath = path.join(__dirname, '..', 'conf', 'ghbot.abcomg');
|
||||
let commands = parseCommands(commandsFilePath);
|
||||
fs.watchFile(commandsFilePath, (curr, prev) => {
|
||||
if (curr.mtime !== prev.mtime) {
|
||||
commands = require(commandsFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
function exists(command)
|
||||
{
|
||||
let matches = commands.filter(obj => { return obj.Command === cmdPrefix+command; });
|
||||
if (matches.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function get(command)
|
||||
{
|
||||
let matches = commands.filter(obj => { return obj.Command === cmdPrefix+command; });
|
||||
if (matches.length > 0) {
|
||||
let match = matches[0];
|
||||
if (match.Enabled === true) {
|
||||
return matches[0].Response;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Read in the array of objects exported from AnkhBot
|
||||
function parseCommands(filePath)
|
||||
{
|
||||
let data = fs.readFileSync(filePath, 'utf-8');
|
||||
let commands = eval(data);
|
||||
return commands;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
module.exports = {
|
||||
get: isOnCooldown,
|
||||
set: placeOnCooldown
|
||||
};
|
||||
|
||||
const NodeCache = require('node-cache');
|
||||
const cache = new NodeCache({checkperiod: 1});
|
||||
const md5 = require('md5');
|
||||
const keyPrefix = 'cd';
|
||||
|
||||
// Given a cooldownTime in seconds and a command, returns false if the command is not on cooldown
|
||||
// returns the time in seconds until the command will be ready again otherwise
|
||||
function isOnCooldown(command, cooldownTime, callback)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
let now = Date.now();
|
||||
let onCooldown = false;
|
||||
let key = keyPrefix + md5(command);
|
||||
|
||||
cache.get(key, function(err, timeUsed) {
|
||||
if (err) reject(err);
|
||||
|
||||
if (timeUsed !== null) {
|
||||
// Command was recently used, check timestamp to see if it's on cooldown
|
||||
if ((now - timeUsed) <= (cooldownTime*1000)) {
|
||||
// Calculate how much longer it's on cooldown
|
||||
onCooldown = ((cooldownTime*1000) - (now - timeUsed))/1000;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(onCooldown);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Places a command on cooldown for cooldownTime (in seconds)
|
||||
function placeOnCooldown(command, cooldownTime)
|
||||
{
|
||||
let key = keyPrefix + md5(command);
|
||||
return cache.set(key, Date.now(), cooldownTime, handleCacheSet);
|
||||
}
|
||||
|
||||
function handleCacheSet(error, result) {}
|
||||
|
||||
process.on('exit', (code) => {cache.close()});
|
||||
324
lib/fgfm.js
324
lib/fgfm.js
@@ -1,324 +0,0 @@
|
||||
const util = require('./util'),
|
||||
emitter = require('events').EventEmitter,
|
||||
sysutil = require('util');
|
||||
|
||||
function FGFM(config) {
|
||||
// Set up initial state
|
||||
this.config = config.config;
|
||||
this.obs = config.obs;
|
||||
this.state = {
|
||||
showStatus: 'IDLE',
|
||||
videoQueue: [],
|
||||
recentlyPlayed: [],
|
||||
currentVideo: null,
|
||||
videoTimer: null,
|
||||
lastCommercialShownAt: Date.now(),
|
||||
commercialPlaying: false
|
||||
};
|
||||
|
||||
emitter.call(this);
|
||||
|
||||
this.startingSoon = (streamStartDelaySeconds, showStartDelaySeconds) => {
|
||||
// @TODO: Move these defaults to config
|
||||
if (typeof streamStartDelaySeconds === 'undefined') {
|
||||
streamStartDelaySeconds = 1;
|
||||
} else {
|
||||
streamStartDelaySeconds = parseInt(streamStartDelaySeconds);
|
||||
}
|
||||
|
||||
if (typeof showStartDelaySeconds === 'undefined') {
|
||||
showStartDelaySeconds = 300;
|
||||
} else {
|
||||
showStartDelaySeconds = parseInt(showStartDelaySeconds);
|
||||
showStartDelaySeconds += streamStartDelaySeconds;
|
||||
}
|
||||
|
||||
this.state.showStatus = 'STARTING_SOON';
|
||||
|
||||
// Set up the initial queue by randomly choosing the configured amount of vods included in shuffling
|
||||
this.state.videoQueue = this.config.vods.alttp.filter(e => e.includeInShuffle === true).sort(util.randSort).slice(0, this.config.initialQueueSize);
|
||||
|
||||
// Show the starting-soon scene
|
||||
this.obs.switchToScene('starting-soon');
|
||||
|
||||
// Restore volume
|
||||
this.obs.setVolume('headphones', 1.0);
|
||||
|
||||
// Start the stream after delay
|
||||
console.log(`The stream will start in ${streamStartDelaySeconds} seconds!`);
|
||||
setTimeout(() => {this.obs.startStream().then(() => {this.emit('STREAM_STARTED')})}, streamStartDelaySeconds*1000);
|
||||
|
||||
// Start the "show" after stream+show delay
|
||||
// @TODO: Actually show the countdown in the scene
|
||||
console.log(`The show will start in ${showStartDelaySeconds} seconds!`);
|
||||
setTimeout(this.startTheShow, showStartDelaySeconds*1000);
|
||||
};
|
||||
|
||||
// Set up initial queue + start playback
|
||||
this.startTheShow = () => {
|
||||
// Set up the initial queue by randomly choosing the configured amount of vods included in shuffling
|
||||
this.state.videoQueue = this.config.vods.alttp.filter(e => e.includeInShuffle === true).sort(util.randSort).slice(0, this.config.initialQueueSize);
|
||||
|
||||
// Start queue playback
|
||||
this.state.currentVideo = this.state.videoQueue.shift();
|
||||
this.showVideo(this.state.currentVideo);
|
||||
|
||||
// restore volume
|
||||
this.obs.setVolume('headphones', 1.0);
|
||||
|
||||
this.state.showStatus = 'RUNNING';
|
||||
|
||||
this.emit('SHOW_STARTED');
|
||||
};
|
||||
|
||||
this.endTheShow = (creditsDelaySeconds, endDelaySeconds) => {
|
||||
if (typeof creditsDelaySeconds === 'undefined' || creditsDelaySeconds === false) {
|
||||
creditsDelaySeconds = 0;
|
||||
}
|
||||
|
||||
if (typeof endDelaySeconds === 'undefined' || endDelaySeconds === false) {
|
||||
endDelaySeconds = 60;
|
||||
}
|
||||
|
||||
console.log(`Credits will be shown in ${creditsDelaySeconds} seconds!`);
|
||||
this.emit('SHOW_ENDING', creditsDelaySeconds);
|
||||
|
||||
let end = () => {
|
||||
this.state.showStatus = 'ENDING';
|
||||
|
||||
// Hide current video, don't play next video
|
||||
this.obs.hide(this.state.currentVideo.sceneItem, this.config.defaultSceneName)
|
||||
clearTimeout(this.state.videoTimer);
|
||||
|
||||
this.obs.switchToScene('credits')
|
||||
.then(() => {
|
||||
this.emit('CREDITS_SHOWN', endDelaySeconds);
|
||||
|
||||
if (endDelaySeconds < 5) endDelaySeconds = 5;
|
||||
console.log(`Stream will stop in ${endDelaySeconds} seconds`);
|
||||
let fadeOutDelay = endDelaySeconds - 5;
|
||||
|
||||
// Fade out volume with 5 seconds left
|
||||
setTimeout(() => {
|
||||
this.obs.getVolume('headphones')
|
||||
.then(currentVolume => {
|
||||
console.log(`current volume of headphones: ${currentVolume}`);
|
||||
let step = 0.1;
|
||||
while (currentVolume > 0.1) {
|
||||
currentVolume -= step;
|
||||
console.log(`setting volume to: ${currentVolume}`);
|
||||
this.obs.setVolume('headphones', currentVolume);
|
||||
util.sleep(250);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, fadeOutDelay*1000);
|
||||
|
||||
setTimeout(() => {
|
||||
this.obs.stopStream();
|
||||
this.state.showStatus = 'ENDED';
|
||||
this.emit('SHOW_ENDED');
|
||||
}, endDelaySeconds*1000);
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
if (creditsDelaySeconds > 0) {
|
||||
setTimeout(end, creditsDelaySeconds*1000);
|
||||
} else {
|
||||
end();
|
||||
}
|
||||
};
|
||||
|
||||
// Shows.. a... video
|
||||
this.showVideo = video => {
|
||||
console.log(`Showing video: ${video.chatName}`);
|
||||
|
||||
this.obs.playVideoInScene(video, this.config.defaultSceneName, this.nextVideo)
|
||||
.then(timer => {
|
||||
// track timer so we can cancel callback later on if necessary
|
||||
this.state.videoTimer = timer;
|
||||
|
||||
// update activity label and show/hide appropriately
|
||||
if (video.hasOwnProperty('label') && video.label !== false) {
|
||||
this.obs.showActivity(video.label);
|
||||
} else {
|
||||
this.obs.hideActivity();
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
// Adds a gameplay vod to the queue
|
||||
this.addVideo = video => {
|
||||
return this.state.videoQueue.push(video);
|
||||
};
|
||||
|
||||
// Adds a room to the queue and handles looping setup
|
||||
this.addRoomVideo = (room, loop) => {
|
||||
let loops = 1;
|
||||
if (typeof loop === 'undefined' || loop === true) {
|
||||
loops = Math.floor(this.config.roomVidPlaytime / room.videoData.length);
|
||||
}
|
||||
console.log(`Adding room video for ${room.dungeonName} - ${room.roomName} to the queue (${loops} loops)`);
|
||||
|
||||
let video = {
|
||||
filePath: `${this.config.roomVidsBasePath}${room.winPath}`,
|
||||
sceneItem: (room.videoData.width === 960) ? "4x3ph" : "16x9ph",
|
||||
length: room.videoData.length,
|
||||
label: room.roomName,
|
||||
chatName: room.roomName,
|
||||
loops: loops,
|
||||
requestedBy: room.requestedBy
|
||||
};
|
||||
|
||||
this.state.videoQueue.push(video);
|
||||
};
|
||||
|
||||
// Picks the next video in the queue (shuffles if empty)
|
||||
// Also handles "commercial breaks" if enabled
|
||||
this.nextVideo = () => {
|
||||
// @TODO: Validate this.state.showStatus -- make sure the "show" hasn't been paused or stopped
|
||||
let ignoreStates = ['ENDING', 'ENDED', 'PAUSED'];
|
||||
if (ignoreStates.includes(this.state.showStatus)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show a "commercial break" if it's been long enough since the last one
|
||||
let secondsSinceLastCommercial = (Date.now() - this.state.lastCommercialShownAt) / 1000;
|
||||
if (this.config.commercialsEnabled === true && secondsSinceLastCommercial >= this.config.commercialInterval) {
|
||||
console.log(`It has been ${secondsSinceLastCommercial} seconds since the last commercial break!`);
|
||||
// Random chance for it to be "everybody wow"
|
||||
let memeId = false;
|
||||
if ((Math.floor(Math.random() * 100) + 1) <= this.config.auwChance) {
|
||||
console.log(`Showing AUW!`);
|
||||
memeId = 'auw';
|
||||
}
|
||||
|
||||
this.showMeme(memeId)
|
||||
.then(() => {
|
||||
this.state.lastCommercialShownAt = Date.now();
|
||||
this.nextVideo();
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep track of recently played videos
|
||||
if (this.state.recentlyPlayed.length === this.config.recentlyPlayedMemory) {
|
||||
this.state.recentlyPlayed.shift();
|
||||
}
|
||||
this.state.recentlyPlayed.push(this.state.currentVideo.id);
|
||||
|
||||
// If a commercial is playing, wait until it's done to switch
|
||||
while (this.state.commercialPlaying === true) {}
|
||||
|
||||
// play the next video in the queue, or pick one at random if the queue is empty
|
||||
if (this.state.videoQueue.length > 0) {
|
||||
this.state.currentVideo = this.state.videoQueue.shift();
|
||||
} else {
|
||||
// Random chance for room grind to be played for an amount of time instead of another video be shuffled to
|
||||
if ((Math.floor(Math.random() * 100) + 1) <= this.config.roomGrindChance) {
|
||||
console.log(`Room grind selected!`);
|
||||
// show room-grind source
|
||||
this.obs.showRoomGrind(this.config.roomGrindPlaytime, () => {this.nextVideo()})
|
||||
.then(timer => {
|
||||
this.state.videoTimer = timer;
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Random chance for room videos to be added
|
||||
if ((Math.floor(Math.random() * 100) + 1) <= this.config.roomShuffleChance) {
|
||||
console.log(`Room vids selected!`);
|
||||
|
||||
this.addRoomVideo(this.config.rooms.sort(util.randSort).slice(0, 1).shift());
|
||||
|
||||
// play the first one
|
||||
this.state.currentVideo = this.state.videoQueue.shift();
|
||||
} else {
|
||||
// filter recently played from shuffle
|
||||
let freshVods = this.config.vods.alttp.filter(e => {
|
||||
return e.includeInShuffle === true && !this.state.recentlyPlayed.includes(e.id);
|
||||
});
|
||||
this.state.currentVideo = freshVods.sort(util.randSort).slice(0, 1).shift();
|
||||
}
|
||||
}
|
||||
|
||||
this.showVideo(this.state.currentVideo);
|
||||
};
|
||||
|
||||
// "Commercials"
|
||||
this.showCommercial = (video, callback) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let handleFinish = () => {
|
||||
console.log('commercial is finished playing...');
|
||||
this.state.commercialPlaying = false;
|
||||
if (typeof callback !== 'undefined') callback();
|
||||
}
|
||||
|
||||
this.obs.playVideoInScene(video, this.config.commercialSceneName, handleFinish)
|
||||
.then(timer => {
|
||||
this.state.commercialPlaying = true;
|
||||
resolve(timer);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
// Memes-By-Id
|
||||
this.showMeme = id => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// find the vod in memes
|
||||
let video = this.config.vods.memes.find(e => e.id === id);
|
||||
if (!video) {
|
||||
reject(`No meme found matching ID ${id}`);
|
||||
}
|
||||
|
||||
let handleFinish = () => {
|
||||
if (id === 'auw') {
|
||||
this.obs.hide("owen", this.config.commercialSceneName);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.showCommercial(video, handleFinish)
|
||||
.then(videoHasStarted => {
|
||||
// in the case of 'auw', show owen
|
||||
if (id === 'auw') {
|
||||
this.obs.show("owen", this.config.commercialSceneName);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
};
|
||||
|
||||
// Skip the current video and play the next
|
||||
this.skip = () => {
|
||||
clearTimeout(this.state.videoTimer);
|
||||
this.obs.hide(this.state.currentVideo.sceneItem, this.config.defaultSceneName).then(this.nextVideo).catch(console.error);
|
||||
};
|
||||
|
||||
// Clears.. the... queue
|
||||
this.clearQueue = () => {
|
||||
this.state.videoQueue = [];
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.state.showStatus = 'PAUSED';
|
||||
this.emit('SHOW_PAUSED');
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
this.state.showStatus = 'RUNNING';
|
||||
this.nextVideo();
|
||||
this.emit('SHOW_RESUMED');
|
||||
};
|
||||
}
|
||||
|
||||
sysutil.inherits(FGFM, emitter);
|
||||
|
||||
module.exports = FGFM;
|
||||
178
lib/ghobs.js
178
lib/ghobs.js
@@ -1,178 +0,0 @@
|
||||
const OBSWebSocket = require('obs-websocket-js');
|
||||
|
||||
function GHOBS(config) {
|
||||
this.config = config;
|
||||
this.websocket = new OBSWebSocket();
|
||||
|
||||
this.init = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Connecting to OBS Websocket...`);
|
||||
this.websocket.connect({ address: this.config.obs.websocket.address, password: this.config.obs.websocket.password })
|
||||
.then(() => {
|
||||
console.log(`Success! We're connected to OBS!`);
|
||||
this.websocket.getCurrentScene().then(currentScene => this.currentScene = currentScene.name);
|
||||
this.websocket.onSwitchScenes(newScene => this.currentScene = newScene.sceneName);
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
|
||||
// Listen for errors from OBS
|
||||
// @TODO: Handle socket disconnect gracefully
|
||||
/** { status: 'error',
|
||||
description: 'There is no Socket connection available.',
|
||||
code: 'NOT_CONNECTED',
|
||||
error: 'There is no Socket connection available.' }*/
|
||||
this.websocket.on('error', err => {
|
||||
console.error(`OBS websocket error: ${JSON.stringify(err)}`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.startStream = () => {
|
||||
return this.websocket.startStreaming();
|
||||
};
|
||||
|
||||
this.stopStream = () => {
|
||||
return this.websocket.stopStreaming();
|
||||
};
|
||||
|
||||
this.setVolume = (source, volume) => {
|
||||
return this.websocket.setVolume({source: source, volume: volume});
|
||||
}
|
||||
|
||||
this.getVolume = (source) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.websocket.getVolume({source: source})
|
||||
.then(res => {
|
||||
resolve(res.volume);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Plays a video in the current scene and hides when finished
|
||||
this.playVideo = (video, callback) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// @TODO Validation of video
|
||||
|
||||
// set the file path on the source
|
||||
let sourceSettings = {
|
||||
local_file: video.filePath,
|
||||
looping: (typeof video.loops !== 'undefined' && video.loops > 1)
|
||||
};
|
||||
sourceSettings.loop = sourceSettings.looping;
|
||||
|
||||
this.websocket.setSourceSettings({"sourceName": video.sceneItem, "sourceSettings": sourceSettings})
|
||||
// show the video scene item
|
||||
.then(() => this.websocket.setSceneItemProperties({"item": video.sceneItem, "visible": true}))
|
||||
// when the video is over, hide it and trigger the user callback, but resolve promise immediately with the timer
|
||||
.then(() => {
|
||||
// if this video is being looped, adjust timeout length to allow the requested number of loops to complete
|
||||
if (sourceSettings.loop === true) {
|
||||
video.length *= video.loops;
|
||||
}
|
||||
|
||||
// resolve Promise with a timer of when the video will finish playback
|
||||
// trigger user callback when the video finishes
|
||||
let timer = setTimeout(() => {
|
||||
this.websocket.setSceneItemProperties({"item": video.sceneItem, "visible": false});
|
||||
if (typeof callback !== 'undefined') {
|
||||
callback();
|
||||
}
|
||||
}, parseInt(video.length*1000));
|
||||
|
||||
resolve(timer);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Shows a video in the given scene/item and then hides it and switches back to the original scene when finished
|
||||
this.playVideoInScene = (video, scene, callback) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
video.scene = scene;
|
||||
let originalScene = this.currentScene || false;
|
||||
let handleVideoEnd = () => {
|
||||
if (originalScene !== false) {
|
||||
this.websocket.setCurrentScene({"scene-name": originalScene});
|
||||
}
|
||||
if (typeof callback !== 'undefined') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
this.websocket.setCurrentScene({"scene-name": scene})
|
||||
.then(() => this.playVideo(video, handleVideoEnd))
|
||||
.then(timer => { resolve(timer) })
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
this.showActivity = (newActivity) => {
|
||||
let update = {
|
||||
"source": this.config.currentActivitySceneItemName,
|
||||
"scene-name": this.config.defaultSceneName,
|
||||
"render": true
|
||||
};
|
||||
|
||||
if (typeof newActivity !== 'undefined' && newActivity.length > 0) {
|
||||
update.text = newActivity;
|
||||
}
|
||||
|
||||
return this.websocket.setTextGDIPlusProperties(update);
|
||||
};
|
||||
|
||||
this.hideActivity = () => {
|
||||
return this.websocket.setSceneItemProperties({"item": this.config.currentActivitySceneItemName, "scene-name": this.config.defaultSceneName, "visible": false});
|
||||
};
|
||||
|
||||
this.showRoomGrind = (playTime, callback) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.websocket.setSceneItemProperties({"item": "room-grind", "scene-name": this.config.defaultSceneName, "visible": true})
|
||||
.then(res => {
|
||||
this.showActivity("NOW SHOWING: TTAS Room Grind !ttas");
|
||||
resolve(setTimeout(() => {
|
||||
// after timeout, hide room-grind and call user callback
|
||||
this.websocket.setSceneItemProperties({"item": "room-grind", "scene-name": this.config.defaultSceneName, "visible": false});
|
||||
if (typeof callback !== 'undefined') callback();
|
||||
}, playTime*1000));
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
this.setVisible = (item, scene, visible) => {
|
||||
return this.websocket.setSceneItemProperties({"item": item, "scene-name": scene, "visible": visible});
|
||||
};
|
||||
|
||||
this.toggleVisible = (item) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.websocket.getSceneItemProperties({"item": item})
|
||||
.then(data => {
|
||||
let newVisibility = !data.visible;
|
||||
this.websocket.setSceneItemProperties({"item": item, "visible": newVisibility}).then(resolve);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
this.show = (item, scene) => {
|
||||
return this.setVisible(item, scene, true);
|
||||
};
|
||||
|
||||
this.hide = (item, scene) => {
|
||||
return this.setVisible(item, scene, false);
|
||||
};
|
||||
|
||||
this.switchToScene = (scene) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.currentScene === scene) {
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
this.websocket.setCurrentScene({"scene-name": scene}).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = GHOBS;
|
||||
134
lib/spotify.js
134
lib/spotify.js
@@ -1,134 +0,0 @@
|
||||
var SpotifyWebApi = require('spotify-web-api-node');
|
||||
|
||||
function Spotify(config) {
|
||||
// Set up initial state
|
||||
this.config = config;
|
||||
|
||||
this.credentials = {
|
||||
clientId: this.config.clientId,
|
||||
clientSecret: this.config.clientSecret,
|
||||
redirectUri: this.config.redirectUri
|
||||
};
|
||||
|
||||
const spotifyApi = new SpotifyWebApi(this.credentials);
|
||||
|
||||
// The code that's returned as a query parameter to the redirect URI
|
||||
const code = this.config.userCode;
|
||||
|
||||
this.init = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Retrieve an access token and a refresh token
|
||||
spotifyApi.authorizationCodeGrant(code).then(
|
||||
function(data) {
|
||||
console.log('The token expires in ' + data.body['expires_in']);
|
||||
console.log('The access token is ' + data.body['access_token']);
|
||||
console.log('The refresh token is ' + data.body['refresh_token']);
|
||||
|
||||
// Set the access token on the API object to use it in later calls
|
||||
spotifyApi.setAccessToken(data.body['access_token']);
|
||||
spotifyApi.setRefreshToken(data.body['refresh_token']);
|
||||
|
||||
// clientId, clientSecret and refreshToken has been set on the api object previous to this call.
|
||||
setInterval(() => {
|
||||
spotifyApi.refreshAccessToken().then(
|
||||
function(data) {
|
||||
console.log('The access token has been refreshed!');
|
||||
|
||||
// Save the access token so that it's used in future calls
|
||||
spotifyApi.setAccessToken(data.body['access_token']);
|
||||
},
|
||||
function(err) {
|
||||
console.log('Could not refresh access token', err);
|
||||
}
|
||||
);
|
||||
}, data.body['expires_in']*1000);
|
||||
|
||||
resolve();
|
||||
},
|
||||
function(err) {
|
||||
console.log('Something went wrong!', JSON.stringify(err));
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
this.getMe = () => {
|
||||
spotifyApi.getMe()
|
||||
.then(function(data) {
|
||||
console.log('Some information about the authenticated user', data.body);
|
||||
}, function(err) {
|
||||
console.log('Something went wrong!', err);
|
||||
});
|
||||
};
|
||||
|
||||
this.getCurrentSong = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
spotifyApi.getMyCurrentPlaybackState({}, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let state = data.body;
|
||||
resolve({
|
||||
artists: state.item.artists,
|
||||
name: state.item.name,
|
||||
album: state.item.album.name,
|
||||
url: state.item.external_urls.spotify
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.getCurrentPlaylist = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
spotifyApi.getMyCurrentPlaybackState({}, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let state = data.body;
|
||||
if (state.context) {
|
||||
resolve(state.context.external_urls.spotify);
|
||||
} else {
|
||||
resolve(state.item.album.external_urls.spotify);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.playContext = (uri) => {
|
||||
return spotifyApi.play({"context_uri": uri});
|
||||
};
|
||||
|
||||
this.skip = () => {
|
||||
return spotifyApi.skipToNext();
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
return spotifyApi.pause();
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
return spotifyApi.play();
|
||||
};
|
||||
|
||||
this.setVolume = (volume) => {
|
||||
volume = parseInt(volume);
|
||||
if (volume < 0) volume = 0;
|
||||
if (volume > 100) volume = 100;
|
||||
return spotifyApi.setVolume(volume);
|
||||
};
|
||||
|
||||
this.shuffle = (state) => {
|
||||
return spotifyApi.setShuffle({"state": state});
|
||||
};
|
||||
|
||||
this.repeat = (state) => {
|
||||
return spotifyApi.setRepeat({"state": state});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Spotify;
|
||||
@@ -1,53 +0,0 @@
|
||||
module.exports = {
|
||||
exists: exists,
|
||||
get: get
|
||||
};
|
||||
|
||||
const fs = require('fs'),
|
||||
path = require('path');
|
||||
|
||||
const commandsFilePath = path.join(__dirname, '..', 'conf', 'text_commands');
|
||||
|
||||
// Read in basic text commands / definitions and watch for changes
|
||||
let commands = parseCommands(commandsFilePath);
|
||||
fs.watchFile(commandsFilePath, (curr, prev) => {
|
||||
if (curr.mtime !== prev.mtime) {
|
||||
commands = parseCommands(commandsFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
// Read/parse text commands from the "database"
|
||||
function parseCommands(filePath)
|
||||
{
|
||||
let commands = {};
|
||||
let data = fs.readFileSync(filePath, 'utf-8');
|
||||
let commandLines = data.toString().split('\n');
|
||||
let commandParts;
|
||||
let aliases;
|
||||
commandLines.forEach(function(line) {
|
||||
if (line.length > 0 && line.indexOf('|') !== -1) {
|
||||
commandParts = line.split('|');
|
||||
// check for aliases
|
||||
aliases = commandParts[0].split(',');
|
||||
aliases.forEach(function(cmd) {
|
||||
commands[cmd] = commandParts[1];
|
||||
//console.log(`!${cmd},`);
|
||||
});
|
||||
}
|
||||
});
|
||||
return commands;
|
||||
}
|
||||
|
||||
function exists(command)
|
||||
{
|
||||
return commands.hasOwnProperty(command);
|
||||
}
|
||||
|
||||
function get(command)
|
||||
{
|
||||
if (exists(command)) {
|
||||
return commands[command];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
const util = require('util'),
|
||||
emitter = require('events').EventEmitter;
|
||||
|
||||
function Timers()
|
||||
{
|
||||
let self = this;
|
||||
|
||||
emitter.call(self);
|
||||
|
||||
self.once = (forTimestamp, eventName) => {
|
||||
// figure out ms between now and scheduled time
|
||||
// setTimeout for event to be fired at that time
|
||||
let diff = forTimestamp - Date.now();
|
||||
if (diff < 0) return;
|
||||
setTimeout(() => {self.emit(eventName)}, diff);
|
||||
return self;
|
||||
};
|
||||
|
||||
self.repeat = (intervalSeconds, eventName) => {
|
||||
setInterval(() => {self.emit(eventName)}, intervalSeconds*1000);
|
||||
return self;
|
||||
};
|
||||
|
||||
self.onceAndRepeat = (forTimestamp, intervalSeconds, eventName) => {
|
||||
let diff = forTimestamp - Date.now();
|
||||
if (diff < 0) return self;
|
||||
setTimeout(() => {
|
||||
self.emit(eventName);
|
||||
self.repeat(intervalSeconds, eventName);
|
||||
}, diff);
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
util.inherits(Timers, emitter);
|
||||
|
||||
module.exports = new Timers();
|
||||
60
lib/util.js
60
lib/util.js
@@ -1,60 +0,0 @@
|
||||
// Converts seconds to human-readable time
|
||||
String.prototype.toHHMMSS = function () {
|
||||
let sec_num = parseInt(this, 10); // don't forget the second param
|
||||
let hours = Math.floor(sec_num / 3600);
|
||||
let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
let seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {hours = "0"+hours;}
|
||||
if (minutes < 10) {minutes = "0"+minutes;}
|
||||
if (seconds < 10) {seconds = "0"+seconds;}
|
||||
return hours+':'+minutes+':'+seconds;
|
||||
};
|
||||
|
||||
var exports = module.exports = {};
|
||||
|
||||
exports.asyncForEach = async function(array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array)
|
||||
}
|
||||
};
|
||||
|
||||
exports.range = function(start,stop) {
|
||||
var result=[];
|
||||
for (var idx=start.charCodeAt(0),end=stop.charCodeAt(0); idx <=end; ++idx){
|
||||
result.push(String.fromCharCode(idx));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
exports.randElement = function(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
};
|
||||
|
||||
exports.sum = function(e) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < e.length; i++) {
|
||||
sum += parseInt(e[i], 10);
|
||||
}
|
||||
|
||||
return sum;
|
||||
};
|
||||
|
||||
exports.average = function(e) {
|
||||
let sum = exports.sum(e);
|
||||
|
||||
let avg = sum / e.length;
|
||||
|
||||
return avg;
|
||||
};
|
||||
|
||||
exports.randSort = () => { return 0.5 - Math.random() };
|
||||
|
||||
exports.sleep = (milliseconds) => {
|
||||
var start = new Date().getTime();
|
||||
for (var i = 0; i < 1e7; i++) {
|
||||
if ((new Date().getTime() - start) > milliseconds){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
868
package-lock.json
generated
868
package-lock.json
generated
@@ -1,868 +0,0 @@
|
||||
{
|
||||
"name": "ghbot",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
|
||||
"requires": {
|
||||
"co": "4.6.0",
|
||||
"fast-deep-equal": "1.1.0",
|
||||
"fast-json-stable-stringify": "2.0.0",
|
||||
"json-schema-traverse": "0.3.1"
|
||||
}
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"requires": {
|
||||
"safer-buffer": "2.1.2"
|
||||
}
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
|
||||
"integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
|
||||
"requires": {
|
||||
"lodash": "4.17.11"
|
||||
}
|
||||
},
|
||||
"async-limiter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
|
||||
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
},
|
||||
"babel-polyfill": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
|
||||
"integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"core-js": "2.5.7",
|
||||
"regenerator-runtime": "0.10.5"
|
||||
}
|
||||
},
|
||||
"babel-runtime": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||
"requires": {
|
||||
"core-js": "2.5.7",
|
||||
"regenerator-runtime": "0.11.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tweetnacl": "0.14.5"
|
||||
}
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
|
||||
"integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
|
||||
},
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
|
||||
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
|
||||
"requires": {
|
||||
"delayed-stream": "1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
||||
"integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM="
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
|
||||
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
|
||||
},
|
||||
"cookiejar": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
|
||||
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
|
||||
},
|
||||
"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="
|
||||
},
|
||||
"cron-parser": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.6.0.tgz",
|
||||
"integrity": "sha512-KGfDDTjBIx85MnVYcdhLccoJH/7jcYW+5Z/t3Wsg2QlJhmmjf+97z+9sQftS71lopOYYapjEKEvmWaCsym5Z4g==",
|
||||
"requires": {
|
||||
"is-nan": "1.2.1",
|
||||
"moment-timezone": "0.5.21"
|
||||
}
|
||||
},
|
||||
"crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0"
|
||||
}
|
||||
},
|
||||
"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-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"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"discord.js": {
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.4.2.tgz",
|
||||
"integrity": "sha512-MDwpu0lMFTjqomijDl1Ed9miMQe6kB4ifKdP28QZllmLv/HVOJXhatRgjS8urp/wBlOfx+qAYSXcdI5cKGYsfg==",
|
||||
"requires": {
|
||||
"long": "4.0.0",
|
||||
"prism-media": "0.0.3",
|
||||
"snekfetch": "3.6.4",
|
||||
"tweetnacl": "1.0.0",
|
||||
"ws": "4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tweetnacl": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz",
|
||||
"integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"jsbn": "0.1.1",
|
||||
"safer-buffer": "2.1.2"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
|
||||
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||
},
|
||||
"ffmpeg-binaries": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ffmpeg-binaries/-/ffmpeg-binaries-3.2.2.tgz",
|
||||
"integrity": "sha1-Nw8wIO9rTbpipVGkJcY01sdaOiE="
|
||||
},
|
||||
"fluent-ffmpeg": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz",
|
||||
"integrity": "sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=",
|
||||
"requires": {
|
||||
"async": "2.6.1",
|
||||
"which": "1.3.1"
|
||||
}
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"requires": {
|
||||
"asynckit": "0.4.0",
|
||||
"combined-stream": "1.0.7",
|
||||
"mime-types": "2.1.21"
|
||||
}
|
||||
},
|
||||
"formidable": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
|
||||
"integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg=="
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0"
|
||||
}
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz",
|
||||
"integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
|
||||
"requires": {
|
||||
"ajv": "5.5.2",
|
||||
"har-schema": "2.0.0"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"jsprim": "1.4.1",
|
||||
"sshpk": "1.14.2"
|
||||
}
|
||||
},
|
||||
"iconv": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv/-/iconv-2.2.3.tgz",
|
||||
"integrity": "sha1-4ITWDut9c9p/CpwJbkyKvgkL+u0=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "2.10.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"irc": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/irc/-/irc-0.5.2.tgz",
|
||||
"integrity": "sha1-NxT0doNlqW0LL3dryRFmvrJGS7w=",
|
||||
"requires": {
|
||||
"iconv": "2.2.3",
|
||||
"irc-colors": "1.4.3",
|
||||
"node-icu-charset-detector": "0.2.0"
|
||||
}
|
||||
},
|
||||
"irc-colors": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/irc-colors/-/irc-colors-1.4.3.tgz",
|
||||
"integrity": "sha512-VeAnFC9fkb4nB/s6UtTNf3BH2wOk/sSBSzIzCpFwrgoFxVl6J5sov7FXXA0kmbk/mVAZQXfXAsQFjWnGPf4cRg=="
|
||||
},
|
||||
"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-nan": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.2.1.tgz",
|
||||
"integrity": "sha1-n69ltvttskt/XAYoR16nH5iEAeI=",
|
||||
"requires": {
|
||||
"define-properties": "1.1.3"
|
||||
}
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
},
|
||||
"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="
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||
"optional": true
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
|
||||
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.2.3",
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"long-timeout": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
|
||||
"integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ="
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
|
||||
"requires": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "1.1.6"
|
||||
}
|
||||
},
|
||||
"memcache": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/memcache/-/memcache-0.3.0.tgz",
|
||||
"integrity": "sha1-vbuXjqS+4P3TFmmXsYg9KX4IWdw="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"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.37.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
|
||||
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.21",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
|
||||
"integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
|
||||
"requires": {
|
||||
"mime-db": "1.37.0"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
|
||||
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
|
||||
},
|
||||
"moment-timezone": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz",
|
||||
"integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==",
|
||||
"requires": {
|
||||
"moment": "2.22.2"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
|
||||
},
|
||||
"node-cache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-4.2.0.tgz",
|
||||
"integrity": "sha512-obRu6/f7S024ysheAjoYFEEBqqDWv4LOMNJEuO8vMeEw2AT4z+NCzO4hlc2lhI4vATzbCQv6kke9FVdx0RbCOw==",
|
||||
"requires": {
|
||||
"clone": "2.1.2",
|
||||
"lodash": "4.17.11"
|
||||
}
|
||||
},
|
||||
"node-icu-charset-detector": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/node-icu-charset-detector/-/node-icu-charset-detector-0.2.0.tgz",
|
||||
"integrity": "sha1-wjINo3Tdy2cfxUy0oOBB4Vb/1jk=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "2.10.0"
|
||||
}
|
||||
},
|
||||
"node-opus": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/node-opus/-/node-opus-0.2.9.tgz",
|
||||
"integrity": "sha512-+IIOdav5D7vHsuLDNk55t17kK2s6c1w4DbfKw8UQxZ635n+AO/SqDE3RpuO3PZKqpWjPtL/chzYZNVxz8/4TUQ==",
|
||||
"requires": {
|
||||
"bindings": "1.2.1",
|
||||
"commander": "2.11.0",
|
||||
"nan": "2.10.0",
|
||||
"ogg-packet": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node-schedule": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.0.tgz",
|
||||
"integrity": "sha512-NNwO9SUPjBwFmPh3vXiPVEhJLn4uqYmZYvJV358SRGM06BR4UoIqxJpeJwDDXB6atULsgQA97MfD1zMd5xsu+A==",
|
||||
"requires": {
|
||||
"cron-parser": "2.6.0",
|
||||
"long-timeout": "0.1.1",
|
||||
"sorted-array-functions": "1.2.0"
|
||||
}
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
|
||||
},
|
||||
"object-keys": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
|
||||
"integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag=="
|
||||
},
|
||||
"obs-websocket-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-1.2.0.tgz",
|
||||
"integrity": "sha1-aE/Br0r+JlV4wXd2dqW7LKKrwGw=",
|
||||
"requires": {
|
||||
"babel-polyfill": "6.26.0",
|
||||
"debug": "3.1.0",
|
||||
"sha.js": "2.4.11",
|
||||
"ws": "5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
|
||||
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
|
||||
"requires": {
|
||||
"async-limiter": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ogg-packet": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ogg-packet/-/ogg-packet-1.0.0.tgz",
|
||||
"integrity": "sha1-RbiFchrI991c8iOR1CEGrlM6xng=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ref-struct": "1.1.0"
|
||||
}
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
},
|
||||
"prism-media": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-0.0.3.tgz",
|
||||
"integrity": "sha512-c9KkNifSMU/iXT8FFTaBwBMr+rdVcN+H/uNv1o+CuFeTThNZNTOrQ+RgXA1yL/DeLk098duAeRPP3QNPNbhxYQ=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
|
||||
},
|
||||
"psl": {
|
||||
"version": "1.1.29",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
|
||||
"integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
"inherits": "2.0.3",
|
||||
"isarray": "1.0.0",
|
||||
"process-nextick-args": "2.0.0",
|
||||
"safe-buffer": "5.1.2",
|
||||
"string_decoder": "1.1.1",
|
||||
"util-deprecate": "1.0.2"
|
||||
}
|
||||
},
|
||||
"ref": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ref/-/ref-1.3.5.tgz",
|
||||
"integrity": "sha512-2cBCniTtxcGUjDpvFfVpw323a83/0RLSGJJY5l5lcomZWhYpU2cuLdsvYqMixvsdLJ9+sTdzEkju8J8ZHDM2nA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "1.2.1",
|
||||
"debug": "2.6.9",
|
||||
"nan": "2.10.0"
|
||||
}
|
||||
},
|
||||
"ref-struct": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ref-struct/-/ref-struct-1.1.0.tgz",
|
||||
"integrity": "sha1-XV7mWtQc78Olxf60BYcmHkee3BM=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"ref": "1.3.5"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
|
||||
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
|
||||
},
|
||||
"request": {
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"requires": {
|
||||
"aws-sign2": "0.7.0",
|
||||
"aws4": "1.8.0",
|
||||
"caseless": "0.12.0",
|
||||
"combined-stream": "1.0.6",
|
||||
"extend": "3.0.2",
|
||||
"forever-agent": "0.6.1",
|
||||
"form-data": "2.3.2",
|
||||
"har-validator": "5.1.0",
|
||||
"http-signature": "1.2.0",
|
||||
"is-typedarray": "1.0.0",
|
||||
"isstream": "0.1.2",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"mime-types": "2.1.19",
|
||||
"oauth-sign": "0.9.0",
|
||||
"performance-now": "2.1.0",
|
||||
"qs": "6.5.2",
|
||||
"safe-buffer": "5.1.2",
|
||||
"tough-cookie": "2.4.3",
|
||||
"tunnel-agent": "0.6.0",
|
||||
"uuid": "3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"combined-stream": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
|
||||
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
|
||||
"requires": {
|
||||
"delayed-stream": "1.0.0"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
|
||||
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
|
||||
"requires": {
|
||||
"asynckit": "0.4.0",
|
||||
"combined-stream": "1.0.6",
|
||||
"mime-types": "2.1.19"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.35.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz",
|
||||
"integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.19",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz",
|
||||
"integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==",
|
||||
"requires": {
|
||||
"mime-db": "1.35.0"
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"requires": {
|
||||
"inherits": "2.0.3",
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"snekfetch": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz",
|
||||
"integrity": "sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw=="
|
||||
},
|
||||
"sorted-array-functions": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz",
|
||||
"integrity": "sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg=="
|
||||
},
|
||||
"spotify-web-api-node": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spotify-web-api-node/-/spotify-web-api-node-4.0.0.tgz",
|
||||
"integrity": "sha512-FQAX4qiP9xfjmJpkSfF5PEVr7RVorUZiLvcdVTlhVFLYAmQ8VSsZlyb0yTK0GExKhAcgJy9GfWxqjSB2r9SrjA==",
|
||||
"requires": {
|
||||
"superagent": "3.8.3"
|
||||
}
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
|
||||
"integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
|
||||
"requires": {
|
||||
"asn1": "0.2.4",
|
||||
"assert-plus": "1.0.0",
|
||||
"bcrypt-pbkdf": "1.0.2",
|
||||
"dashdash": "1.14.1",
|
||||
"ecc-jsbn": "0.1.2",
|
||||
"getpass": "0.1.7",
|
||||
"jsbn": "0.1.1",
|
||||
"safer-buffer": "2.1.2",
|
||||
"tweetnacl": "0.14.5"
|
||||
}
|
||||
},
|
||||
"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.2"
|
||||
}
|
||||
},
|
||||
"superagent": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||
"integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
|
||||
"requires": {
|
||||
"component-emitter": "1.2.1",
|
||||
"cookiejar": "2.1.2",
|
||||
"debug": "3.2.6",
|
||||
"extend": "3.0.2",
|
||||
"form-data": "2.3.3",
|
||||
"formidable": "1.2.1",
|
||||
"methods": "1.1.2",
|
||||
"mime": "1.6.0",
|
||||
"qs": "6.5.2",
|
||||
"readable-stream": "2.3.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.1"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"requires": {
|
||||
"psl": "1.1.29",
|
||||
"punycode": "1.4.1"
|
||||
}
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
||||
"optional": true
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "1.3.0"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz",
|
||||
"integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==",
|
||||
"requires": {
|
||||
"async-limiter": "1.0.0",
|
||||
"safe-buffer": "5.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
package.json
Executable file → Normal file
51
package.json
Executable file → Normal file
@@ -1,28 +1,41 @@
|
||||
{
|
||||
"name": "ghbot",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"main": "src/index.js",
|
||||
"dependencies": {
|
||||
"async": "^2.6.1",
|
||||
"discord.js": "^11.4.2",
|
||||
"ffmpeg-binaries": "^3.2.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"irc": "^0.5.2",
|
||||
"md5": "^2.2.1",
|
||||
"memcache": "^0.3.0",
|
||||
"moment": "^2.22.2",
|
||||
"node-cache": "^4.2.0",
|
||||
"node-opus": "^0.2.9",
|
||||
"node-schedule": "^1.3.0",
|
||||
"obs-websocket-js": "^1.2.0",
|
||||
"request": "^2.88.0",
|
||||
"spotify-web-api-node": "^4.0.0"
|
||||
"@discordjs/opus": "^0.9.0",
|
||||
"@discordjs/voice": "^0.18.0",
|
||||
"axios": "^1.11.0",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"discord.js": "^14.21.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"node-schedule": "^2.1.1",
|
||||
"opusscript": "^0.1.1",
|
||||
"sodium-native": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.17.2",
|
||||
"nodemon": "^3.1.10"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@discordjs/opus": "npm:opusscript@^0.1.1"
|
||||
}
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"dev": "nodemon src/index.js",
|
||||
"start": "docker compose up -d",
|
||||
"start:logs": "pnpm start && pnpm logs",
|
||||
"stop": "docker compose down",
|
||||
"build": "docker compose build",
|
||||
"restart": "docker compose restart",
|
||||
"logs": "docker compose logs -f",
|
||||
"boom": "pnpm stop && pnpm build && pnpm start",
|
||||
"reset-db": "pnpm stop && rm -f data/*.db* && echo 'Database reset complete. Run pnpm start to re-seed from seed.json'",
|
||||
"image:build": "docker build -t ghbot:${VERSION:-latest} .",
|
||||
"image:run": "docker run -d --name ghbot --restart always ghbot:${VERSION:-latest}"
|
||||
},
|
||||
"author": "",
|
||||
"author": "https://github.com/greenham",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
1031
pnpm-lock.yaml
generated
Normal file
1031
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
const rooms = require('./conf/rooms.json');
|
||||
const util = require('./lib/util');
|
||||
const fs = require('fs');
|
||||
|
||||
populateDatabase();
|
||||
|
||||
async function populateDatabase() {
|
||||
let database = "'ID','Dungeon','Room'\r\n";
|
||||
await util.asyncForEach(rooms, async (room, index) => {
|
||||
let entry = `'${room.id}','${room.dungeonName||'?'}','${room.roomName||'?'}'\r\n`;
|
||||
database += entry;
|
||||
console.log('added entry', entry);
|
||||
});
|
||||
|
||||
fs.writeFile('rooms-list.csv', database, 'utf8', () => {console.log('done')});
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('./lib/util');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
|
||||
let roomVidPath = `/var/hypnoadmin/media/videos/ALttP/my-vids/room-vids`;
|
||||
|
||||
const getAllFiles = dir =>
|
||||
fs.readdirSync(dir).reduce((files, file) => {
|
||||
const name = path.join(dir, file);
|
||||
const isDirectory = fs.statSync(name).isDirectory();
|
||||
return isDirectory ? [...files, ...getAllFiles(name)] : [...files, name];
|
||||
}, []);
|
||||
|
||||
let roomVidFiles = getAllFiles(roomVidPath);
|
||||
|
||||
populateDatabase();
|
||||
|
||||
async function populateDatabase() {
|
||||
let database = [];
|
||||
await util.asyncForEach(roomVidFiles, async (file, index) => {
|
||||
// ignore anything that's not an mp4
|
||||
let shortPath = file.replace(roomVidPath, '');
|
||||
if (!/\.mp4$/.test(shortPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entry = {
|
||||
id: index+1,
|
||||
shortPath: shortPath,
|
||||
winPath: shortPath.replace(/\//g, '\\')
|
||||
};
|
||||
|
||||
// chop up the short path and extract metadata
|
||||
let matches = shortPath.match(/^\/([0-9]{2})-([a-z]+)\/([0-9]{2})-(.+)\.mp4/);
|
||||
if (matches) {
|
||||
entry.dungeonId = matches[1];
|
||||
entry.dungeonName = matches[2];
|
||||
entry.roomId = matches[3];
|
||||
entry.roomName = matches[4];
|
||||
}
|
||||
|
||||
// @TODO support some other paths / structures
|
||||
|
||||
entry.videoData = await getVideoMetadata(file);
|
||||
database.push(entry);
|
||||
console.log('added entry', entry);
|
||||
});
|
||||
|
||||
fs.writeFile('conf/rooms.json', JSON.stringify(database), 'utf8', () => {console.log('done')});
|
||||
}
|
||||
|
||||
function getVideoMetadata(videoPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ffmpeg.ffprobe(videoPath, (err, metadata) => {
|
||||
// find the video stream
|
||||
let stream = metadata.streams.find(e => e.codec_type === "video");
|
||||
if (!stream) {
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
resolve({
|
||||
width: stream.width,
|
||||
height: stream.height,
|
||||
fps: parseInt(stream.r_frame_rate.replace('/1', '')),
|
||||
length: stream.duration
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
818
rooms-list.csv
818
rooms-list.csv
@@ -1,818 +0,0 @@
|
||||
"ID","Dungeon","Room"
|
||||
"0","escape","links-house-2911"
|
||||
"1","escape","outside-links-house-723"
|
||||
"2","escape","outside-castle-1628"
|
||||
"3","escape","outside-castle-1631"
|
||||
"4","escape","uncle-1342"
|
||||
"5","escape","uncle-1348"
|
||||
"6","escape","passage-exit-442"
|
||||
"7","escape","courtyard-923"
|
||||
"8","escape","castle-lobby-631"
|
||||
"9","escape","castle-lobby-pumpless-636"
|
||||
"10","escape","sw-room-439"
|
||||
"11","escape","sw-room-pumpless-448"
|
||||
"12","escape","west-guard-hallway1-603"
|
||||
"13","escape","statue-hallway1-415"
|
||||
"14","escape","1st-keyguard-leftslash-801"
|
||||
"15","escape","first-keyguard1-3slash-825"
|
||||
"16","escape","first-keyguard1-upspin-811"
|
||||
"17","escape","b1-pit-lower-404"
|
||||
"18","escape","b1-pit2-lower-945"
|
||||
"19","escape","stealth-room-927"
|
||||
"20","escape","stealth-room-929"
|
||||
"21","escape","green-guard-bestrng-432"
|
||||
"22","escape","green-guard-bestrng-rta-438"
|
||||
"23","escape","blue-boomguard-739"
|
||||
"24","escape","blue-boomguard-skip-627"
|
||||
"25","escape","green-guard2-426"
|
||||
"26","escape","stairs-to-b2-1-359"
|
||||
"27","escape","b2-1-533"
|
||||
"28","escape","bnc-1pot4slash-3227"
|
||||
"29","escape","bnc-8slash-3120"
|
||||
"30","escape","bnc-8slash-3130"
|
||||
"31","escape","bnc-joestrat-3238"
|
||||
"32","escape","bnc-joestrat-3244"
|
||||
"33","escape","b2-2-530"
|
||||
"34","escape","stairs-to-b2-2-651"
|
||||
"35","escape","stairs-to-b2-2-652"
|
||||
"36","escape","green-guard3-426"
|
||||
"37","escape","stealth-room2-920"
|
||||
"38","escape","b1-pit2-upper-301"
|
||||
"39","escape","b1-pit1-upper-627"
|
||||
"40","escape","b1-pit1-upper-pumpless-628"
|
||||
"41","escape","1st-keyguard2-314"
|
||||
"42","escape","statue-hallway2-709"
|
||||
"43","escape","west-guard-hallway2-612"
|
||||
"44","escape","sw-room2-445"
|
||||
"45","escape","castle-lobby2-721"
|
||||
"46","escape","throne-room-1632"
|
||||
"47","escape","throne-room-1634"
|
||||
"48","escape","sewer-passage-857"
|
||||
"49","escape","sewer-passage-pumpless-903"
|
||||
"50","escape","snake-room-700"
|
||||
"51","escape","snake-room-perfect-rng-655"
|
||||
"52","escape","sewer-key-chest-risky-1212"
|
||||
"53","escape","sewer-key-chest-rta-1215"
|
||||
"54","escape","sewer1-706"
|
||||
"55","escape","sewer1-707"
|
||||
"56","escape","sewer2-608"
|
||||
"57","escape","keyrat-840"
|
||||
"58","escape","rat-hallway-827"
|
||||
"59","escape","behind-sanc-705"
|
||||
"60","escape","behind-sanc-rta-719"
|
||||
"61","escape","behind-sanc2-720"
|
||||
"62","?","?"
|
||||
"63","eastern","sanc-2946"
|
||||
"64","eastern","sanc-heart-3303"
|
||||
"65","eastern","sanc-heart-chestturn-3305"
|
||||
"66","eastern","outside-sanc-807"
|
||||
"67","eastern","graveyard-646"
|
||||
"68","eastern","east-path-403"
|
||||
"69","eastern","wooden-bridge-906"
|
||||
"70","eastern","octofield-724"
|
||||
"71","eastern","guard-bridge-311"
|
||||
"72","eastern","east-fairy-cave-right-exit-326"
|
||||
"73","eastern","outside-eastern-2540"
|
||||
"74","eastern","entrance-504"
|
||||
"75","eastern","three-popos-434"
|
||||
"76","eastern","cannonballs-859"
|
||||
"77","eastern","bigchest-upper-leftpot-736"
|
||||
"78","eastern","westwing-upper-455"
|
||||
"79","eastern","stalfos-spawn-617"
|
||||
"80","eastern","compass-room-615"
|
||||
"81","eastern","westwing-lower1-335"
|
||||
"82","eastern","bigchest-lower1-arrows-805"
|
||||
"83","eastern","bigchest-lower1-noarrows-704"
|
||||
"84","eastern","east-wing-645"
|
||||
"85","eastern","dark-af-boom-623"
|
||||
"86","eastern","dark-af-sword-630"
|
||||
"87","eastern","dark-potkey-diagboom-656"
|
||||
"88","eastern","dark-potkey-walk-658"
|
||||
"89","eastern","dark-af2-607"
|
||||
"90","eastern","cannonballs-upper-851"
|
||||
"91","eastern","bigkey-1105"
|
||||
"92","eastern","westwing-lower2-756"
|
||||
"93","eastern","bigchest-bow-1452"
|
||||
"94","eastern","gwg-noslash-menu-913"
|
||||
"95","eastern","afpots-802"
|
||||
"96","eastern","eyegore-floor-switch-519"
|
||||
"97","eastern","eyegore-floor-switch-pumpless-522"
|
||||
"98","eastern","cannonball-switch-503"
|
||||
"99","eastern","cannonball-switch-pumpless-507"
|
||||
"100","eastern","zgr-601"
|
||||
"101","eastern","double-red-eyegores-617"
|
||||
"102","eastern","armos-topright-1321"
|
||||
"103","desert","outside-eastern-1322"
|
||||
"104","desert","saha-walkout-1951-449-8"
|
||||
"105","desert","outside-eastern2-1301-99-0"
|
||||
"106","desert","east-fairy-cave-253"
|
||||
"107","desert","guard-bridge-601-35-0"
|
||||
"108","desert","links-yard-354-32-0"
|
||||
"109","desert","annoying-bushes-403"
|
||||
"110","desert","west-of-swamp1-356"
|
||||
"111","desert","south-of-grove1-right-exit-452-32-0"
|
||||
"112","desert","grove-westside1-nocharge-255"
|
||||
"113","desert","grove-westside1-swordcharge-308"
|
||||
"114","desert","outside-library1-dashes-708"
|
||||
"115","desert","outside-library1-spindash-626"
|
||||
"116","desert","library-1038-127-0"
|
||||
"117","desert","outside-library2-noss-751-99-0"
|
||||
"118","desert","outside-library2-spindash-707"
|
||||
"119","desert","grove-westside2-leftexit-233-36"
|
||||
"120","desert","grove-westside2-rta-243-36"
|
||||
"121","desert","south-of-grove2-fromleft-belowbush-458-32"
|
||||
"122","desert","south-of-grove2-fromleft-bushdash-455-32"
|
||||
"123","desert","south-of-grove2-fromright-bushdash-447-32"
|
||||
"124","desert","west-of-swamp2-frombelow-350"
|
||||
"125","desert","west-of-swamp2-frombushes-346"
|
||||
"126","desert","west-of-swamp2-fromlowbushes-346"
|
||||
"127","desert","west-of-swamp2-fromlowbushes-farleftexit-346"
|
||||
"128","desert","annoying-bushes2-352-33"
|
||||
"129","desert","links-yard-339-32"
|
||||
"130","desert","links-yard-earlydash-336-32"
|
||||
"131","desert","waterdash-menu-629-35-1"
|
||||
"132","desert","waterdash-nomenu-524-37-0"
|
||||
"133","desert","east-of-watergate-216-35"
|
||||
"134","desert","watergate-552-37"
|
||||
"135","desert","middle-aged-man-643-31"
|
||||
"136","desert","outside-desert-2250-175"
|
||||
"137","desert","desert1-risky-726-97"
|
||||
"138","desert","desert1-rta-730-98"
|
||||
"139","desert","north-hall1-leftpoke-510-21"
|
||||
"140","desert","north-hall1-rtapoke-512-21"
|
||||
"141","desert","nw-room1-258-19"
|
||||
"142","desert","torchkey-rta-614"
|
||||
"143","desert","torchkey-underleft-614"
|
||||
"144","desert","nw-room2-235"
|
||||
"145","desert","north-hall2-412-21"
|
||||
"146","desert","ne-room1-351"
|
||||
"147","desert","east-wing1-416-21"
|
||||
"148","desert","compass-room1-519"
|
||||
"149","desert","bigkey-chestturn-758-49"
|
||||
"150","desert","bigkey-rta-goodrng-803"
|
||||
"151","desert","compass-room2-405"
|
||||
"152","desert","compass-room2-pumpless-407"
|
||||
"153","desert","east-wing2-318"
|
||||
"154","desert","east-wing2-pumpless-319"
|
||||
"155","desert","ne-room2-418-20"
|
||||
"156","desert","north-hall3-412-21"
|
||||
"157","desert","nw-room3-346-18"
|
||||
"158","desert","cop1-528"
|
||||
"159","desert","bigchest-606-29-1"
|
||||
"160","desert","cop2-355"
|
||||
"161","desert","nw-room4-dashes-348"
|
||||
"162","desert","nw-room4-walk-rta-349"
|
||||
"163","desert","west-wing-451-21"
|
||||
"164","desert","d1-exit-324"
|
||||
"165","desert","outside-desert-noss-709-102"
|
||||
"166","desert","outside-desert-spindash-732-102-3"
|
||||
"167","desert","d2-entrance-528-96"
|
||||
"168","desert","d2-entrance-pumpless-532-97"
|
||||
"169","desert","tile-room1-458"
|
||||
"170","desert","bridge-539-58"
|
||||
"171","desert","popos-546"
|
||||
"172","desert","beamos-hall-706-1"
|
||||
"173","desert","beamos-hall-rta-720"
|
||||
"174","desert","tile-room2-517-21"
|
||||
"175","desert","torches-nomenu-1549-0-0"
|
||||
"176","desert","torches-rta-1656-0-4"
|
||||
"177","desert","lanmos-2cyc-1953"
|
||||
"178","desert","lanmos-2cyc-2059"
|
||||
"179","hera","outside-desert-cactusdash-1044-77-0"
|
||||
"180","hera","outside-desert-geldmandash-1053-76-0"
|
||||
"181","hera","outside-desert-rta-highdash-1109-76-0"
|
||||
"182","hera","outside-desert-rta-lowdash-1116-76-0"
|
||||
"183","hera","middle-aged-man-634-30-0"
|
||||
"184","hera","watergate-458-34-0"
|
||||
"185","hera","beach-445-35-0"
|
||||
"186","hera","beach-rta-530-35-0"
|
||||
"187","hera","fakeflips-832-36-0"
|
||||
"188","hera","fakeflips-rta-808-35-0"
|
||||
"189","hera","whirlpool-enter-336-36-0"
|
||||
"190","hera","whirlpool-exit-806-72-0"
|
||||
"191","hera","rupee-tree-picklearrow-808-35-0"
|
||||
"192","hera","rupee-tree-rta-821-35-0"
|
||||
"193","hera","old-man-cave-1026-134-0"
|
||||
"194","hera","old-man-tunnel-2139-295-8"
|
||||
"195","hera","death-mountain-lower-3039-246-009"
|
||||
"196","hera","bunny-link-739"
|
||||
"197","hera","death-mountain-upper-ledgehop-736-76-0"
|
||||
"198","hera","death-mountain-upper-rta-748-76-0"
|
||||
"199","hera","lobby1-503-101-0"
|
||||
"200","hera","lobby1-pumpless-507-101-0"
|
||||
"201","hera","basement-key-629-71-0"
|
||||
"202","hera","lobby2-boom-859-107-007"
|
||||
"203","hera","lobby2-boomdash-851-107-010"
|
||||
"204","hera","lobby2-boomless-824"
|
||||
"205","hera","tileroom-boomdash-4340-66-0"
|
||||
"206","hera","tileroom-boomless-4326"
|
||||
"207","hera","3mold-boom-menu-429-0-2"
|
||||
"208","hera","3mold-boomless-538"
|
||||
"209","hera","torches-812-31-008"
|
||||
"210","hera","lobby3-540-107-0"
|
||||
"211","hera","beetles-936-63-0"
|
||||
"212","hera","bkdoor-342-0-0"
|
||||
"213","hera","bkdoor-pumpless-345-0-0"
|
||||
"214","hera","safety-switch-634"
|
||||
"215","hera","safety-switch-pumpless-dash-639"
|
||||
"216","hera","safety-switch-pumpless-walk-639"
|
||||
"217","hera","bigchest-ebj-2143-95-17"
|
||||
"218","hera","bigchest-waffle-rta-2324-95-20"
|
||||
"219","hera","bigchest-waffle-sparkmanip-2301-95-18"
|
||||
"220","hera","bumper-skip-729-66-0"
|
||||
"221","hera","moldorm-2752-116-0"
|
||||
"222","hera","moldorm-2858-116-0"
|
||||
"223","atower","death-mountain-upper-943"
|
||||
"224","atower","kak-tunnel-01-806"
|
||||
"225","atower","kak-tunnel-02-607"
|
||||
"226","atower","kak-tunnel-02-arrows-835"
|
||||
"227","atower","kak-tunnel-02-rta-710"
|
||||
"228","atower","first-rupee-tree-928"
|
||||
"229","atower","first-rupee-tree-birddash-1029"
|
||||
"230","atower","first-rupee-tree-rta-x4FA-exit-921"
|
||||
"231","atower","lumberjacks-652"
|
||||
"232","atower","lumberjacks-picklearrow-y078-enter-455"
|
||||
"233","atower","lumberjacks-rta-y078-enter-701"
|
||||
"234","atower","lost-woods-01-1709"
|
||||
"235","atower","lost-woods-01-y078-enter-1713"
|
||||
"236","atower","master-sword-3909"
|
||||
"237","atower","lost-woods-02-2252"
|
||||
"238","atower","lost-woods-02-rta-2243"
|
||||
"239","atower","fortune-teller-hut-rta-619"
|
||||
"240","atower","whirlpool-pond-348"
|
||||
"241","atower","wsb-543"
|
||||
"242","atower","outside-hyrule-castle-1000"
|
||||
"243","atower","castle-lobby-abovetorch-531"
|
||||
"244","atower","castle-lobby-rta-540"
|
||||
"245","atower","sw-room-455-bad-recording"
|
||||
"246","atower","sw-room-456"
|
||||
"247","atower","ramparts-617"
|
||||
"248","atower","entrance-334"
|
||||
"249","atower","goldknights-pui-734"
|
||||
"250","atower","prizepack-guards-703"
|
||||
"251","atower","first-dark-room-605"
|
||||
"252","atower","first-dark-room-rta-619"
|
||||
"253","atower","despair-1617-44"
|
||||
"254","atower","last-3f-dark-room-436"
|
||||
"255","atower","first-4f-dark-room-625"
|
||||
"256","atower","first-4f-dark-room-pumpless-628"
|
||||
"257","atower","melancholy-rta-715"
|
||||
"258","atower","archerkey-beams-keydash-534"
|
||||
"259","atower","archerkey-beams-rta-558"
|
||||
"260","atower","redspearguards-809"
|
||||
"261","atower","alert-guards-arrowkill-516"
|
||||
"262","atower","alert-guards-beamkill-528"
|
||||
"263","atower","cop-menu-638"
|
||||
"264","atower","neopolitan-room-325"
|
||||
"265","atower","statue-room-646"
|
||||
"266","atower","catwalk-boom-624"
|
||||
"267","atower","catwalk-boomless-621-bad-recording"
|
||||
"268","atower","7f-hallway-521"
|
||||
"269","atower","agah-cutscene-2451"
|
||||
"270","atower","agah-0bb-4451"
|
||||
"271","pod","pyramid-1043"
|
||||
"272","pod","dark-octo-319"
|
||||
"273","pod","hammer-bridge-444"
|
||||
"274","pod","dark-east-fairy-cave-250"
|
||||
"275","pod","outside-4157"
|
||||
"276","pod","entrance1-dash-554"
|
||||
"277","pod","entrance1-walk-556"
|
||||
"278","pod","west-bomb-pickup-336"
|
||||
"279","pod","shooter-key-847"
|
||||
"280","pod","entrance2-333"
|
||||
"281","pod","bomb-pickup1-428"
|
||||
"282","pod","junction1-ledgehop-524"
|
||||
"283","pod","junction1-slashjelly-556"
|
||||
"284","pod","turtle-key-619"
|
||||
"285","pod","entrance3-333"
|
||||
"286","pod","bomb-pickup2-412"
|
||||
"287","pod","junction2-907"
|
||||
"288","pod","junction2-badrng-917"
|
||||
"289","pod","upper-turtle-key-715"
|
||||
"290","pod","bigkey-leftside-754"
|
||||
"291","pod","entrance4-333"
|
||||
"292","pod","bomb-pickup3-412"
|
||||
"293","pod","junction3-651"
|
||||
"294","pod","beetle-room1-954"
|
||||
"295","pod","beetle-room1-dash2chest-1002"
|
||||
"296","pod","beetle-room1-rta-1012"
|
||||
"297","pod","hammeryump-1bombpickup-1324"
|
||||
"298","pod","hammeryump-2bombpickup-1348"
|
||||
"299","pod","darkmaze-738"
|
||||
"300","pod","entrance5-333"
|
||||
"301","pod","bomb-pickup4-412"
|
||||
"302","pod","junction4-651"
|
||||
"303","pod","beetle-room2-dash-and-hop-644"
|
||||
"304","pod","beetle-room2-rta-walk-702"
|
||||
"305","pod","beetle-room2-walk-and-hop-657"
|
||||
"306","pod","sexy-statue-1333"
|
||||
"307","pod","ugly-statue-1406"
|
||||
"308","pod","mimics-454"
|
||||
"309","pod","eye-statue-dashes-2454"
|
||||
"310","pod","eye-statue-walk-rta-2456"
|
||||
"311","pod","darkpegs-607"
|
||||
"312","pod","lonely-turtle-457"
|
||||
"313","pod","turtle-party-701-cropped"
|
||||
"314","pod","turtle-party-701"
|
||||
"315","pod","turtle-party-809"
|
||||
"316","pod","warp-tile-512"
|
||||
"317","pod","turtle-hallway-dash-and-keydash-709"
|
||||
"318","pod","turtle-hallway-rta-walk-and-slash-745"
|
||||
"319","pod","turtle-hallway-walk-and-keydash-720"
|
||||
"320","pod","helma-2739"
|
||||
"321","pod","helma-2813"
|
||||
"322","pod","helma-2830"
|
||||
"323","pod","helma-2843"
|
||||
"324","pod","helma-2927"
|
||||
"325","thieves","outside-pod-2627"
|
||||
"326","thieves","dark-east-fairy-cave-259"
|
||||
"327","thieves","pegbridge-earlydash-639"
|
||||
"328","thieves","pegbridge-safedash-646"
|
||||
"329","thieves","bombshop-359"
|
||||
"330","thieves","annoying-bushes-402"
|
||||
"331","thieves","annoying-bushes-rightcancel-402"
|
||||
"332","thieves","southeast-of-fluteboy-dash-334"
|
||||
"333","thieves","southeast-of-fluteboy-walk-rta-328"
|
||||
"334","thieves","south-of-fluteboy-458"
|
||||
"335","thieves","south-of-fluteboy-walk-rta-508"
|
||||
"336","thieves","fluteboy-rta-1503"
|
||||
"337","thieves","grove-rta-1840"
|
||||
"338","thieves","south-of-grove-445"
|
||||
"339","thieves","grove-westside-236"
|
||||
"340","thieves","usain-bolt-720"
|
||||
"341","thieves","kak-village-centerdash-3335"
|
||||
"342","thieves","kak-village-leftdash-3335"
|
||||
"343","thieves","three-musketeers-3xdash-557"
|
||||
"344","thieves","lost-woods-830"
|
||||
"345","thieves","warp-rock-705"
|
||||
"346","thieves","warp-rock-no-hammerdash-724"
|
||||
"347","thieves","north-of-outcast-748"
|
||||
"348","thieves","voo-943"
|
||||
"349","thieves","sw-quad-badrng-745"
|
||||
"350","thieves","sw-quad-goodrng-736"
|
||||
"351","thieves","nw-quad-333"
|
||||
"352","thieves","ne-quad-badrng-350"
|
||||
"353","thieves","ne-quad-goodrng-337"
|
||||
"354","thieves","se-quad1-551"
|
||||
"355","thieves","bigkey-610"
|
||||
"356","thieves","se-quad2-628"
|
||||
"357","thieves","ne-quad2-733"
|
||||
"358","thieves","stalfos-hallway-hammer-755"
|
||||
"359","thieves","stalfos-hallway-pickup-754"
|
||||
"360","thieves","snek-and-zazaks-335"
|
||||
"361","thieves","conveyor-gibos-north-352"
|
||||
"362","thieves","conveyor-gibos-north-353"
|
||||
"363","thieves","hellway-429"
|
||||
"364","thieves","spikeroom1-hammer-515"
|
||||
"365","thieves","spikeroom1-mikestrat-502"
|
||||
"366","thieves","attic-pot-switch1-badrng-828"
|
||||
"367","thieves","attic-pot-switch1-goodrng-pickup-750"
|
||||
"368","thieves","grasshopper-hall1-1-252"
|
||||
"369","thieves","grasshopper-hall2-1-308"
|
||||
"370","thieves","attic-window-slashgrass-556"
|
||||
"371","thieves","grasshopper-hall2-2-240"
|
||||
"372","thieves","grasshopper-hall1-2-307"
|
||||
"373","thieves","attic-pot-switch2-306"
|
||||
"374","thieves","spikeroom2-606"
|
||||
"375","thieves","hellway2-322"
|
||||
"376","thieves","zazaks-and-gibos-543"
|
||||
"377","thieves","zazaks-and-gibos-545"
|
||||
"378","thieves","conveyor-toilet-bomb-404"
|
||||
"379","thieves","conveyor-toilet-nobomb-321"
|
||||
"380","thieves","bigblock-837"
|
||||
"381","thieves","lonely-zazak-315"
|
||||
"382","thieves","prison-dashes-1053"
|
||||
"383","thieves","prison-walk-1106"
|
||||
"384","thieves","prison-walk-oneslash-1124"
|
||||
"385","thieves","lonely-zazak2-252"
|
||||
"386","thieves","conveyor-jellies1-328"
|
||||
"387","thieves","bigchest-606"
|
||||
"388","thieves","conveyor-jellies2-509"
|
||||
"389","thieves","bigblock2-arrows-412"
|
||||
"390","thieves","bigblock2-noarrows-345"
|
||||
"391","thieves","conveyor-toilet2-dash-hammer-736"
|
||||
"392","thieves","conveyor-toilet2-hammerdash-726"
|
||||
"393","thieves","conveyor-toilet2-rta-751"
|
||||
"394","thieves","stalfos-hallway2-324"
|
||||
"395","thieves","blind-beams-2701"
|
||||
"396","thieves","blind-beams-walkin-2707"
|
||||
"397","skull","voo1-902"
|
||||
"398","skull","voo1-rta-914"
|
||||
"399","skull","300-hut-721"
|
||||
"400","skull","300-hut-rta-720"
|
||||
"401","skull","voo2-dash-628"
|
||||
"402","skull","voo2-walk-630"
|
||||
"403","skull","cursed-dwarf-justpressa-1210"
|
||||
"404","skull","cursed-dwarf-rta-1224"
|
||||
"405","skull","voo3-339"
|
||||
"406","skull","hammerpegs1-820"
|
||||
"407","skull","outside-smiths1-350"
|
||||
"408","skull","smiths1-708"
|
||||
"409","skull","outside-smiths2-237"
|
||||
"410","skull","smiths2-910"
|
||||
"411","skull","outside-smiths3-239"
|
||||
"412","skull","hammerpegs2-342"
|
||||
"413","skull","outside-smiths4-338"
|
||||
"414","skull","smiths3-746"
|
||||
"415","skull","outside-smiths5-236"
|
||||
"416","skull","hammerpegs3-849"
|
||||
"417","skull","voo4-fencedash-441"
|
||||
"418","skull","c-shaped-house-918"
|
||||
"419","skull","voo5-709"
|
||||
"420","skull","moblins-badrng-524"
|
||||
"421","skull","moblins-goodrng-518"
|
||||
"422","skull","skeleton-forest1-1216"
|
||||
"423","skull","mummy-statue-1220"
|
||||
"424","skull","bigkey-badrng-659"
|
||||
"425","skull","bigkey-leftside-620"
|
||||
"426","skull","bigkey-rightside-635"
|
||||
"427","skull","mumm-statue-exit-232"
|
||||
"428","skull","skeleton-forest2-949"
|
||||
"429","skull","bigchest-earlybj-1259"
|
||||
"430","skull","bigchest-rta-bonk-1407"
|
||||
"431","skull","bigchest-rta-bonkless-1406"
|
||||
"432","skull","skeleton-forest3-933"
|
||||
"433","skull","mummy-statue-passthru-427"
|
||||
"434","skull","bumper-hallway-305"
|
||||
"435","skull","potkey-654"
|
||||
"436","skull","skeleton-forest4-738"
|
||||
"437","skull","skeleton-forest4-rta-740"
|
||||
"438","skull","firesnake-keydash-522"
|
||||
"439","skull","potpit-left-442"
|
||||
"440","skull","potpit-right-438"
|
||||
"441","skull","mummy-hellway-744"
|
||||
"442","skull","mummy-hellway-joestrat-812"
|
||||
"443","skull","vineroom-badrng-415"
|
||||
"444","skull","vineroom-goodrng-359"
|
||||
"445","skull","mummy-key-629"
|
||||
"446","skull","mummy-key-rta-633"
|
||||
"447","skull","mothhole-top-422"
|
||||
"448","skull","mothula-1118"
|
||||
"449","skull","mothula-1122"
|
||||
"450","ice","outside-skull-menu-352"
|
||||
"451","ice","lost-woods-dash-758"
|
||||
"452","ice","lost-woods-walk-806"
|
||||
"453","ice","links-house-632"
|
||||
"454","ice","castle-gate-menu-rta-qw-637"
|
||||
"455","ice","castle-gate-menu-walk-qw-639"
|
||||
"456","ice","castle-gate-walk-qw-534"
|
||||
"457","ice","pyramid-659"
|
||||
"458","ice","dark-octofield-236"
|
||||
"459","ice","broken-bridge-738"
|
||||
"460","ice","lonely-ropa-243"
|
||||
"461","ice","ropa-lottery-910"
|
||||
"462","ice","dark-cuckoos-leftgrab-737"
|
||||
"463","ice","dark-cuckoos-rta-746"
|
||||
"464","ice","quake-1925"
|
||||
"465","ice","quake-earlythrow-1858"
|
||||
"466","ice","whirlpool1-dashcancel-605"
|
||||
"467","ice","whirlpool1-nocancel-603"
|
||||
"468","ice","zoras-domain-dashout-5934"
|
||||
"469","ice","zoras-domain-walkout-5933"
|
||||
"470","ice","tiny-warp-dik-605"
|
||||
"471","ice","ice-warp-833"
|
||||
"472","ice","ice-island-358"
|
||||
"473","ice","ice-entrance-438"
|
||||
"474","ice","ice2-508"
|
||||
"475","ice","floor-switch-910"
|
||||
"476","ice","block-intersection1-358"
|
||||
"477","ice","compass-room-144"
|
||||
"478","ice","block-intersection2-456"
|
||||
"479","ice","penguins1-spin-535"
|
||||
"480","ice","penguins1-tmstrat-523"
|
||||
"481","ice","block-intersection3-512"
|
||||
"482","ice","bombable-floor-dboost-916"
|
||||
"483","ice","bombable-floor-nodboost-944"
|
||||
"484","ice","bombable-floor-rta-good-rng-957"
|
||||
"485","ice","big-stalfos-819"
|
||||
"486","ice","conveyor-hellway-709"
|
||||
"487","ice","ipbj-1209"
|
||||
"488","ice","floor-zols-300"
|
||||
"489","ice","penguins2-804"
|
||||
"490","ice","big-spike-405"
|
||||
"491","ice","lonely-firebar-419"
|
||||
"492","ice","lonely-firebar-dash-451"
|
||||
"493","ice","double-freezor-503"
|
||||
"494","ice","big-chest-401"
|
||||
"495","ice","big-pit-351"
|
||||
"496","ice","block-push-hammerpot-badlag-542"
|
||||
"497","ice","block-push-potpickup-badlag-539"
|
||||
"498","ice","statue-room-hammer-both-pots-1323"
|
||||
"499","ice","statue-room-hammer-one-pot-1306"
|
||||
"500","ice","statue-room-pickup-both-pots-1318"
|
||||
"501","ice","statue-room-pickup-one-pot-1304"
|
||||
"502","ice","kholdstare-1poke-1621"
|
||||
"503","ice","kholdstare-2poke-1559"
|
||||
"504","ice","kholdstare-2poke-1602"
|
||||
"505","ice","kholdstare-6slash-1655"
|
||||
"506","swamp","ice-island-343"
|
||||
"507","swamp","lake-hylia-birddash-803"
|
||||
"508","swamp","lake-hylia-walk-812"
|
||||
"509","swamp","links-house-521"
|
||||
"510","swamp","grassy-area-menu-437"
|
||||
"511","swamp","warp-nokill-422"
|
||||
"512","swamp","warp-octokill-437"
|
||||
"513","swamp","postwarp-hammerdash-950"
|
||||
"514","swamp","postwarp-walk-959"
|
||||
"515","swamp","outside-swamp1-noqw-629"
|
||||
"516","swamp","outside-swamp1-qw-617"
|
||||
"517","swamp","outside-watergate-noqw-427"
|
||||
"518","swamp","outside-watergate-qw-407"
|
||||
"519","swamp","watergate-entrance-mirrordash-455"
|
||||
"520","swamp","watergate-entrance-walk-500"
|
||||
"521","swamp","afskip-715"
|
||||
"522","swamp","afskip-bonk-736"
|
||||
"523","swamp","afskip-rta-732"
|
||||
"524","swamp","watergate-entrance2-317"
|
||||
"525","swamp","outside-watergate2-closewarp-236"
|
||||
"526","swamp","outside-watergate2-qw-413"
|
||||
"527","swamp","outside-swamp2-closewarp-424"
|
||||
"528","swamp","outside-swamp2-qw-352"
|
||||
"529","swamp","dungeon-entrance-1017"
|
||||
"530","swamp","potkey1-menu-1050"
|
||||
"531","swamp","potkey1-nomenu-923"
|
||||
"532","swamp","pool1-empty-627"
|
||||
"533","swamp","potkey2-dash-336"
|
||||
"534","swamp","potkey2-walk-339"
|
||||
"535","swamp","pool1-empty2-dash-515"
|
||||
"536","swamp","pool1-empty2-walk-514"
|
||||
"537","swamp","waterlever1-1232"
|
||||
"538","swamp","pool1-full-542"
|
||||
"539","swamp","bigchest1-839"
|
||||
"540","swamp","pool2-empty-710"
|
||||
"541","swamp","potkey3-dash-336"
|
||||
"542","swamp","potkey3-walk-339"
|
||||
"543","swamp","pool2-empty2-554"
|
||||
"544","swamp","bigchest2-759"
|
||||
"545","swamp","waterlever2-1355"
|
||||
"546","swamp","bigchest3-844"
|
||||
"547","swamp","pool2-full1-hammerdash-920"
|
||||
"548","swamp","pool2-full1-rta-939"
|
||||
"549","swamp","jelly-dash-1015"
|
||||
"550","swamp","sociable-firebar-1030"
|
||||
"551","swamp","jellydash-northside-433"
|
||||
"552","swamp","bigkey-719"
|
||||
"553","swamp","jelly-dash2-928"
|
||||
"554","swamp","pool2-full2-dboost-907"
|
||||
"555","swamp","pool2-full2-hammerdash-925"
|
||||
"556","swamp","pool2-full2-rta-936"
|
||||
"557","swamp","bigchest4-2026"
|
||||
"558","swamp","bigchest4-rta-2112"
|
||||
"559","swamp","statue-room-arrows-1205"
|
||||
"560","swamp","statue-room-no-arrows-1127"
|
||||
"561","swamp","red-jelly-2slash-351"
|
||||
"562","swamp","red-jelly-beam-340"
|
||||
"563","swamp","red-jelly-dash-326"
|
||||
"564","swamp","water-lever3-1146"
|
||||
"565","swamp","smallhall-313"
|
||||
"566","swamp","waterfall-room-509"
|
||||
"567","swamp","restock-all-448"
|
||||
"568","swamp","restock-arrows-only-422"
|
||||
"569","swamp","restock-bomb-only-420"
|
||||
"570","swamp","restock-skip-dash-330"
|
||||
"571","swamp","restock-skip-walk-334"
|
||||
"572","swamp","c-room-602"
|
||||
"573","swamp","phelps-way-943"
|
||||
"574","swamp","t-room-234"
|
||||
"575","swamp","arrghus-frqk-1737"
|
||||
"576","swamp","arrghus-frqk-1745"
|
||||
"577","swamp","arrghus-frqk-1748"
|
||||
"578","swamp","arrghus-noqk-1902"
|
||||
"579","swamp","arrghus-noqk-1930"
|
||||
"580","mire","outside-swamp-357"
|
||||
"581","mire","outside-swamp-from-fr-341"
|
||||
"582","mire","outside-swamp-from-fr-with-qw-348"
|
||||
"583","mire","outside-swamp-from-mirror-qw-236"
|
||||
"584","mire","outside-watergate-852"
|
||||
"585","mire","outside-watergate-from-qw-836"
|
||||
"586","mire","dm-both-swordclimbs-2145"
|
||||
"587","mire","dm-hookspeed-noswordclimbs-2202"
|
||||
"588","mire","dm-hybrid-2205"
|
||||
"589","mire","dm-spindash-2123"
|
||||
"590","mire","dm-spindash-2134"
|
||||
"591","mire","dm-spindash-dmgcancel-2111"
|
||||
"592","mire","dark-dm-534"
|
||||
"593","mire","ether-dashoff-2305"
|
||||
"594","mire","ether-quickhop-2243"
|
||||
"595","mire","octoballoon-610"
|
||||
"596","mire","icerod-entrance-931"
|
||||
"597","mire","icerod1-334"
|
||||
"598","mire","icerodchest-nomenu-659"
|
||||
"599","mire","icerodchest-rta-menu-815"
|
||||
"600","mire","icerod2-menu-350"
|
||||
"601","mire","icerod2-rta-nomenu-233"
|
||||
"602","mire","icerod-leaving-547"
|
||||
"603","mire","icerod-leaving-rta-556"
|
||||
"604","mire","mirewarp-menu-701"
|
||||
"605","mire","mire-entrance-nomenu-2501"
|
||||
"606","mire","mire01-821"
|
||||
"607","mire","mire02-1031"
|
||||
"608","mire","mainhub1-toproute-947"
|
||||
"609","mire","poporoom1-337"
|
||||
"610","mire","spikekey1-351"
|
||||
"611","mire","poporoom2-337"
|
||||
"612","mire","beatthefireball-938"
|
||||
"613","mire","jellykey-449"
|
||||
"614","mire","tileroom-321"
|
||||
"615","mire","bombslugs-508"
|
||||
"616","mire","torches1-752"
|
||||
"617","mire","torches2-rta-2127"
|
||||
"618","mire","torches2-toplighting-2121"
|
||||
"619","mire","bighole-322"
|
||||
"620","mire","bigkey-621"
|
||||
"621","mire","warptile-230"
|
||||
"622","mire","wizzroom-412"
|
||||
"623","mire","bigspike-318"
|
||||
"624","mire","lonelystalfos-337"
|
||||
"625","mire","sparkgamble-bestboost-1106"
|
||||
"626","mire","sparkgamble-damageless-1144"
|
||||
"627","mire","sparkgamble-rta-menu-1236"
|
||||
"628","mire","sparkgamble-snekblock-1205"
|
||||
"629","mire","maproom-south-327"
|
||||
"630","mire","maproom-south-rta-327"
|
||||
"631","mire","bigchest-menu-1012"
|
||||
"632","mire","bigchest-nomenu-852"
|
||||
"633","mire","maproom-north-328"
|
||||
"634","mire","spikeykey2-rta-walk-702"
|
||||
"635","mire","spikeykey2-rta-walk-magicgrab-753"
|
||||
"636","mire","spikeykey2-rta-walk-magicgrab-with-hook-752"
|
||||
"637","mire","jadinledge-hookdash-333"
|
||||
"638","mire","jadinledge-quickhop-328"
|
||||
"639","mire","jadinledge-rta-2dash-332"
|
||||
"640","mire","wizzpot-rta-hook-610"
|
||||
"641","mire","wizzpot-topdash-558"
|
||||
"642","mire","bridge-504"
|
||||
"643","mire","caneblockswitch-old-928"
|
||||
"644","mire","caneblockswitch-spooky-934"
|
||||
"645","mire","bigblock-429"
|
||||
"646","mire","caneswitch-spooky-1132"
|
||||
"647","mire","caneswitch-spooky-roddash-bonk-1108"
|
||||
"648","mire","bombwall-spooky-dashout-513"
|
||||
"649","mire","bombwall-spooky-dashout-risky-var-443"
|
||||
"650","mire","bombwall-spooky-walk-534"
|
||||
"651","mire","badfarmroom-spooky-dash-above-612"
|
||||
"652","mire","badfarmroom-spooky-dash-below-608"
|
||||
"653","mire","badfarmroom-spooky-riskier-walk-619"
|
||||
"654","mire","badfarmroom-spooky-rta-walk-624"
|
||||
"655","mire","firesnek-skip-menu-956"
|
||||
"656","mire","firesnek-skip-rta-nomenu-909"
|
||||
"657","mire","firesnek-skip-rta-nomenu-915"
|
||||
"658","mire","vitty-nomenu-1732"
|
||||
"659","mire","vitty-rta-menu-1846"
|
||||
"660","?","?"
|
||||
"661","trock","outside-mire-335"
|
||||
"662","trock","outside-desert-818"
|
||||
"663","trock","wafflehouse-drivethru-1241"
|
||||
"664","trock","wafflehouse-drivethru-nostairclimb-1243"
|
||||
"665","trock","broken-bridge-1034"
|
||||
"666","trock","broken-bridge-fromfacingup-1037"
|
||||
"667","trock","paradox-lower-454"
|
||||
"668","trock","paradox-upper-740"
|
||||
"669","trock","run-killing-deadrocks-525"
|
||||
"670","trock","hammerpegs-1251"
|
||||
"671","trock","outside-trock-2057"
|
||||
"672","trock","outside-trock-rta-1957"
|
||||
"673","trock","dungeon-entrance-menu-933"
|
||||
"674","trock","dungeon-entrance-no-menu-818"
|
||||
"675","trock","largepit1-928"
|
||||
"676","trock","torches-1428"
|
||||
"677","trock","torches-rta-1432"
|
||||
"678","trock","rollerroom-1038"
|
||||
"679","trock","rollerroom-topdmg-1032"
|
||||
"680","trock","torches-backtrack-346"
|
||||
"681","trock","largepit2-1025"
|
||||
"682","trock","pokey0-526"
|
||||
"683","trock","pokey0-rta-551"
|
||||
"684","trock","chomps-2blocks-805"
|
||||
"685","trock","chomps-bbb-758"
|
||||
"686","trock","chomps-blocknbeam-748"
|
||||
"687","trock","tunnels-arrows-2040"
|
||||
"688","trock","tunnels-no-arrows-1948"
|
||||
"689","trock","lava-room1-739"
|
||||
"690","trock","pokey1-bl-keydash-849"
|
||||
"691","trock","bigkey-fromdash-1245"
|
||||
"692","trock","bigkey-fromwalk-1241"
|
||||
"693","trock","pokey1-backtrack-beamless-644"
|
||||
"694","trock","pokey1-backtrack-beams-632"
|
||||
"695","trock","pokey1-backtrack-canedash-624"
|
||||
"696","trock","lava-room3-1247"
|
||||
"697","trock","double-pokeys-429"
|
||||
"698","trock","miniroller-2dash-339"
|
||||
"699","trock","lavaroom4-619"
|
||||
"700","trock","walldash-414"
|
||||
"701","trock","crystalroller-beams-812"
|
||||
"702","trock","darkroom-2428"
|
||||
"703","trock","helmadash1-623"
|
||||
"704","trock","laserskip-block-921"
|
||||
"705","trock","laserskip-dash-823"
|
||||
"706","trock","helmadash2-414"
|
||||
"707","trock","helmadash2-rta-417"
|
||||
"708","trock","canedash-725[30lf]"
|
||||
"709","trock","canedash-725[31lf]"
|
||||
"710","trock","canedash-magicless-814"
|
||||
"711","trock","canedash-rta-walk-823"
|
||||
"712","trock","restock-graball-1238"
|
||||
"713","trock","restock-magicgrab-1232"
|
||||
"714","trock","restock-skipall-1145"
|
||||
"715","trock","trinexx-rta-3759"
|
||||
"716","gtower","outside-trock-454"
|
||||
"717","gtower","lynel-bridge-909"
|
||||
"718","gtower","entrance-2239"
|
||||
"719","gtower","entrance-nostairclimb-2251"
|
||||
"720","gtower","foyer-624"
|
||||
"721","gtower","torchkey-bonkandwalk-759"
|
||||
"722","gtower","torchkey-hook-758"
|
||||
"723","gtower","torchkey-nokey-606"
|
||||
"724","gtower","conveyor-potkey-905"
|
||||
"725","gtower","spikeskip-919"
|
||||
"726","gtower","spikeskip-noskip-937"
|
||||
"727","gtower","double-crystal-switch-719"
|
||||
"728","gtower","double-crystal-switch-rta-739"
|
||||
"729","gtower","double-crystal-switch-rta-740"
|
||||
"730","gtower","spike-and-pegs-356"
|
||||
"731","gtower","firesnake-room-1029"
|
||||
"732","gtower","firesnake-room-bonk-1117"
|
||||
"733","gtower","firesnake-room-framerule-1011"
|
||||
"734","gtower","warp-tile1-157"
|
||||
"735","gtower","warp-tile2-344"
|
||||
"736","gtower","warp-tile2-justholddown-350"
|
||||
"737","gtower","warp-tile3-430"
|
||||
"738","gtower","warp-tileskip-816"
|
||||
"739","gtower","warp-tileskip-with-menu-931"
|
||||
"740","gtower","false-floor-no-menu-1017"
|
||||
"741","gtower","false-floor-with-menu-1122"
|
||||
"742","gtower","bombable-floor-717"
|
||||
"743","gtower","icearmos-1529"
|
||||
"744","gtower","icearmos-321-1548"
|
||||
"745","gtower","icearmos-321-dboost-1546"
|
||||
"746","gtower","bk-arrows-738"
|
||||
"747","gtower","bk-no-arrows-606"
|
||||
"748","gtower","foyer2-745"
|
||||
"749","gtower","mj-room-1045-arrow-dash"
|
||||
"750","gtower","mimics1-joestrat-647"
|
||||
"751","gtower","mimics1-myramics-653"
|
||||
"752","gtower","mimics2-blunt-553"
|
||||
"753","gtower","mimics2-opt-519"
|
||||
"754","gtower","east-spike-room-302"
|
||||
"755","gtower","spiketrap-1341"
|
||||
"756","gtower","spiketrap-abovepot-1406"
|
||||
"757","gtower","gbz-513"
|
||||
"758","gtower","gbz-walk-637"
|
||||
"759","gtower","g1-838"
|
||||
"760","gtower","g2-441"
|
||||
"761","gtower","g3-644"
|
||||
"762","gtower","g4-446"
|
||||
"763","gtower","g5-459"
|
||||
"764","gtower","bunnybeamhall-305"
|
||||
"765","gtower","lanmo2-1141"
|
||||
"766","gtower","restock-248"
|
||||
"767","gtower","wizz1-849"
|
||||
"768","gtower","guardbridge-529"
|
||||
"769","gtower","wizz2-429"
|
||||
"770","gtower","foosdabridge-653"
|
||||
"771","gtower","foosdabridge-rightdash-703"
|
||||
"772","gtower","torches1-1251-magic-noroddash"
|
||||
"773","gtower","eyelasers-529"
|
||||
"774","gtower","torches2-637"
|
||||
"775","gtower","torches2-safewalk-647"
|
||||
"776","gtower","helmakey-500-dmg"
|
||||
"777","gtower","helmakey-507-dmgless"
|
||||
"778","gtower","bombwall-521"
|
||||
"779","gtower","bombwall-damageless-530"
|
||||
"780","gtower","bombwall-rta-543"
|
||||
"781","gtower","pegschestkey-843"
|
||||
"782","gtower","pegschestkey-magicskip-454"
|
||||
"783","gtower","pegschestkey-safe-634"
|
||||
"784","gtower","mold2-hover-1232"
|
||||
"785","gtower","mold2-hover-1301"
|
||||
"786","gtower","mold2-hover-1337"
|
||||
"787","gtower","mold2-kill-hook-1949"
|
||||
"788","gtower","mold2-kill-hook-1958"
|
||||
"789","gtower","helma-hallway-607"
|
||||
"790","gtower","helma-hallway-safe-620"
|
||||
"791","gtower","torch-hallway-635"
|
||||
"792","gtower","agah2-4927"
|
||||
"793","?","?"
|
||||
"794","?","?"
|
||||
"795","?","?"
|
||||
"796","?","?"
|
||||
"797","?","?"
|
||||
"798","ganon","ganon-139.55-nfc"
|
||||
"799","ganon","ganon-139.55"
|
||||
"800","?","?"
|
||||
"801","?","?"
|
||||
"802","?","?"
|
||||
"803","?","?"
|
||||
"804","?","?"
|
||||
"805","?","?"
|
||||
"806","?","?"
|
||||
"807","?","?"
|
||||
"808","?","?"
|
||||
"809","?","?"
|
||||
"810","?","?"
|
||||
"811","?","?"
|
||||
"812","?","?"
|
||||
"813","?","?"
|
||||
"814","?","?"
|
||||
"815","?","?"
|
||||
"816","?","?"
|
||||
|
818
rooms-list.txt
818
rooms-list.txt
@@ -1,818 +0,0 @@
|
||||
'ID','Dungeon','Room'
|
||||
'0','?','links-house-2911'
|
||||
'1','?','outside-links-house-723'
|
||||
'2','?','outside-castle-1628'
|
||||
'3','?','outside-castle-1631'
|
||||
'4','?','uncle-1342'
|
||||
'5','?','uncle-1348'
|
||||
'6','?','passage-exit-442'
|
||||
'7','?','courtyard-923'
|
||||
'8','?','castle-lobby-631'
|
||||
'9','?','castle-lobby-pumpless-636'
|
||||
'10','?','sw-room-439'
|
||||
'11','?','sw-room-pumpless-448'
|
||||
'12','?','west-guard-hallway1-603'
|
||||
'13','?','statue-hallway1-415'
|
||||
'14','?','1st-keyguard-leftslash-801'
|
||||
'15','?','first-keyguard1-3slash-825'
|
||||
'16','?','first-keyguard1-upspin-811'
|
||||
'17','?','b1-pit-lower-404'
|
||||
'18','?','b1-pit2-lower-945'
|
||||
'19','?','stealth-room-927'
|
||||
'20','?','stealth-room-929'
|
||||
'21','?','green-guard-bestrng-432'
|
||||
'22','?','green-guard-bestrng-rta-438'
|
||||
'23','?','blue-boomguard-739'
|
||||
'24','?','blue-boomguard-skip-627'
|
||||
'25','?','green-guard2-426'
|
||||
'26','?','stairs-to-b2-1-359'
|
||||
'27','?','b2-1-533'
|
||||
'28','?','bnc-1pot4slash-3227'
|
||||
'29','?','bnc-8slash-3120'
|
||||
'30','?','bnc-8slash-3130'
|
||||
'31','?','bnc-joestrat-3238'
|
||||
'32','?','bnc-joestrat-3244'
|
||||
'33','?','b2-2-530'
|
||||
'34','?','stairs-to-b2-2-651'
|
||||
'35','?','stairs-to-b2-2-652'
|
||||
'36','?','green-guard3-426'
|
||||
'37','?','stealth-room2-920'
|
||||
'38','?','b1-pit2-upper-301'
|
||||
'39','?','b1-pit1-upper-627'
|
||||
'40','?','b1-pit1-upper-pumpless-628'
|
||||
'41','?','1st-keyguard2-314'
|
||||
'42','?','statue-hallway2-709'
|
||||
'43','?','west-guard-hallway2-612'
|
||||
'44','?','sw-room2-445'
|
||||
'45','?','castle-lobby2-721'
|
||||
'46','?','throne-room-1632'
|
||||
'47','?','throne-room-1634'
|
||||
'48','?','sewer-passage-857'
|
||||
'49','?','sewer-passage-pumpless-903'
|
||||
'50','?','snake-room-700'
|
||||
'51','?','snake-room-perfect-rng-655'
|
||||
'52','?','sewer-key-chest-risky-1212'
|
||||
'53','?','sewer-key-chest-rta-1215'
|
||||
'54','?','sewer1-706'
|
||||
'55','?','sewer1-707'
|
||||
'56','?','sewer2-608'
|
||||
'57','?','keyrat-840'
|
||||
'58','?','rat-hallway-827'
|
||||
'59','?','behind-sanc-705'
|
||||
'60','?','behind-sanc-rta-719'
|
||||
'61','?','behind-sanc2-720'
|
||||
'62','?','?'
|
||||
'63','?','sanc-2946'
|
||||
'64','?','sanc-heart-3303'
|
||||
'65','?','sanc-heart-chestturn-3305'
|
||||
'66','?','outside-sanc-807'
|
||||
'67','?','graveyard-646'
|
||||
'68','?','east-path-403'
|
||||
'69','?','wooden-bridge-906'
|
||||
'70','?','octofield-724'
|
||||
'71','?','guard-bridge-311'
|
||||
'72','?','east-fairy-cave-right-exit-326'
|
||||
'73','?','outside-eastern-2540'
|
||||
'74','?','entrance-504'
|
||||
'75','?','three-popos-434'
|
||||
'76','?','cannonballs-859'
|
||||
'77','?','bigchest-upper-leftpot-736'
|
||||
'78','?','westwing-upper-455'
|
||||
'79','?','stalfos-spawn-617'
|
||||
'80','?','compass-room-615'
|
||||
'81','?','westwing-lower1-335'
|
||||
'82','?','bigchest-lower1-arrows-805'
|
||||
'83','?','bigchest-lower1-noarrows-704'
|
||||
'84','?','east-wing-645'
|
||||
'85','?','dark-af-boom-623'
|
||||
'86','?','dark-af-sword-630'
|
||||
'87','?','dark-potkey-diagboom-656'
|
||||
'88','?','dark-potkey-walk-658'
|
||||
'89','?','dark-af2-607'
|
||||
'90','?','cannonballs-upper-851'
|
||||
'91','?','bigkey-1105'
|
||||
'92','?','westwing-lower2-756'
|
||||
'93','?','bigchest-bow-1452'
|
||||
'94','?','gwg-noslash-menu-913'
|
||||
'95','?','afpots-802'
|
||||
'96','?','eyegore-floor-switch-519'
|
||||
'97','?','eyegore-floor-switch-pumpless-522'
|
||||
'98','?','cannonball-switch-503'
|
||||
'99','?','cannonball-switch-pumpless-507'
|
||||
'100','?','zgr-601'
|
||||
'101','?','double-red-eyegores-617'
|
||||
'102','?','armos-topright-1321'
|
||||
'103','?','outside-eastern-1322'
|
||||
'104','?','saha-walkout-1951-449-8'
|
||||
'105','?','outside-eastern2-1301-99-0'
|
||||
'106','?','east-fairy-cave-253'
|
||||
'107','?','guard-bridge-601-35-0'
|
||||
'108','?','links-yard-354-32-0'
|
||||
'109','?','annoying-bushes-403'
|
||||
'110','?','west-of-swamp1-356'
|
||||
'111','?','south-of-grove1-right-exit-452-32-0'
|
||||
'112','?','grove-westside1-nocharge-255'
|
||||
'113','?','grove-westside1-swordcharge-308'
|
||||
'114','?','outside-library1-dashes-708'
|
||||
'115','?','outside-library1-spindash-626'
|
||||
'116','?','library-1038-127-0'
|
||||
'117','?','outside-library2-noss-751-99-0'
|
||||
'118','?','outside-library2-spindash-707'
|
||||
'119','?','grove-westside2-leftexit-233-36'
|
||||
'120','?','grove-westside2-rta-243-36'
|
||||
'121','?','south-of-grove2-fromleft-belowbush-458-32'
|
||||
'122','?','south-of-grove2-fromleft-bushdash-455-32'
|
||||
'123','?','south-of-grove2-fromright-bushdash-447-32'
|
||||
'124','?','west-of-swamp2-frombelow-350'
|
||||
'125','?','west-of-swamp2-frombushes-346'
|
||||
'126','?','west-of-swamp2-fromlowbushes-346'
|
||||
'127','?','west-of-swamp2-fromlowbushes-farleftexit-346'
|
||||
'128','?','annoying-bushes2-352-33'
|
||||
'129','?','links-yard-339-32'
|
||||
'130','?','links-yard-earlydash-336-32'
|
||||
'131','?','waterdash-menu-629-35-1'
|
||||
'132','?','waterdash-nomenu-524-37-0'
|
||||
'133','?','east-of-watergate-216-35'
|
||||
'134','?','watergate-552-37'
|
||||
'135','?','middle-aged-man-643-31'
|
||||
'136','?','outside-desert-2250-175'
|
||||
'137','?','desert1-risky-726-97'
|
||||
'138','?','desert1-rta-730-98'
|
||||
'139','?','north-hall1-leftpoke-510-21'
|
||||
'140','?','north-hall1-rtapoke-512-21'
|
||||
'141','?','nw-room1-258-19'
|
||||
'142','?','torchkey-rta-614'
|
||||
'143','?','torchkey-underleft-614'
|
||||
'144','?','nw-room2-235'
|
||||
'145','?','north-hall2-412-21'
|
||||
'146','?','ne-room1-351'
|
||||
'147','?','east-wing1-416-21'
|
||||
'148','?','compass-room1-519'
|
||||
'149','?','bigkey-chestturn-758-49'
|
||||
'150','?','bigkey-rta-goodrng-803'
|
||||
'151','?','compass-room2-405'
|
||||
'152','?','compass-room2-pumpless-407'
|
||||
'153','?','east-wing2-318'
|
||||
'154','?','east-wing2-pumpless-319'
|
||||
'155','?','ne-room2-418-20'
|
||||
'156','?','north-hall3-412-21'
|
||||
'157','?','nw-room3-346-18'
|
||||
'158','?','cop1-528'
|
||||
'159','?','bigchest-606-29-1'
|
||||
'160','?','cop2-355'
|
||||
'161','?','nw-room4-dashes-348'
|
||||
'162','?','nw-room4-walk-rta-349'
|
||||
'163','?','west-wing-451-21'
|
||||
'164','?','d1-exit-324'
|
||||
'165','?','outside-desert-noss-709-102'
|
||||
'166','?','outside-desert-spindash-732-102-3'
|
||||
'167','?','d2-entrance-528-96'
|
||||
'168','?','d2-entrance-pumpless-532-97'
|
||||
'169','?','tile-room1-458'
|
||||
'170','?','bridge-539-58'
|
||||
'171','?','popos-546'
|
||||
'172','?','beamos-hall-706-1'
|
||||
'173','?','beamos-hall-rta-720'
|
||||
'174','?','tile-room2-517-21'
|
||||
'175','?','torches-nomenu-1549-0-0'
|
||||
'176','?','torches-rta-1656-0-4'
|
||||
'177','?','lanmos-2cyc-1953'
|
||||
'178','?','lanmos-2cyc-2059'
|
||||
'179','?','outside-desert-cactusdash-1044-77-0'
|
||||
'180','?','outside-desert-geldmandash-1053-76-0'
|
||||
'181','?','outside-desert-rta-highdash-1109-76-0'
|
||||
'182','?','outside-desert-rta-lowdash-1116-76-0'
|
||||
'183','?','middle-aged-man-634-30-0'
|
||||
'184','?','watergate-458-34-0'
|
||||
'185','?','beach-445-35-0'
|
||||
'186','?','beach-rta-530-35-0'
|
||||
'187','?','fakeflips-832-36-0'
|
||||
'188','?','fakeflips-rta-808-35-0'
|
||||
'189','?','whirlpool-enter-336-36-0'
|
||||
'190','?','whirlpool-exit-806-72-0'
|
||||
'191','?','rupee-tree-picklearrow-808-35-0'
|
||||
'192','?','rupee-tree-rta-821-35-0'
|
||||
'193','?','old-man-cave-1026-134-0'
|
||||
'194','?','old-man-tunnel-2139-295-8'
|
||||
'195','?','death-mountain-lower-3039-246-009'
|
||||
'196','?','bunny-link-739'
|
||||
'197','?','death-mountain-upper-ledgehop-736-76-0'
|
||||
'198','?','death-mountain-upper-rta-748-76-0'
|
||||
'199','?','lobby1-503-101-0'
|
||||
'200','?','lobby1-pumpless-507-101-0'
|
||||
'201','?','basement-key-629-71-0'
|
||||
'202','?','lobby2-boom-859-107-007'
|
||||
'203','?','lobby2-boomdash-851-107-010'
|
||||
'204','?','lobby2-boomless-824'
|
||||
'205','?','tileroom-boomdash-4340-66-0'
|
||||
'206','?','tileroom-boomless-4326'
|
||||
'207','?','3mold-boom-menu-429-0-2'
|
||||
'208','?','3mold-boomless-538'
|
||||
'209','?','torches-812-31-008'
|
||||
'210','?','lobby3-540-107-0'
|
||||
'211','?','beetles-936-63-0'
|
||||
'212','?','bkdoor-342-0-0'
|
||||
'213','?','bkdoor-pumpless-345-0-0'
|
||||
'214','?','safety-switch-634'
|
||||
'215','?','safety-switch-pumpless-dash-639'
|
||||
'216','?','safety-switch-pumpless-walk-639'
|
||||
'217','?','bigchest-ebj-2143-95-17'
|
||||
'218','?','bigchest-waffle-rta-2324-95-20'
|
||||
'219','?','bigchest-waffle-sparkmanip-2301-95-18'
|
||||
'220','?','bumper-skip-729-66-0'
|
||||
'221','?','moldorm-2752-116-0'
|
||||
'222','?','moldorm-2858-116-0'
|
||||
'223','?','death-mountain-upper-943'
|
||||
'224','?','kak-tunnel-01-806'
|
||||
'225','?','kak-tunnel-02-607'
|
||||
'226','?','kak-tunnel-02-arrows-835'
|
||||
'227','?','kak-tunnel-02-rta-710'
|
||||
'228','?','first-rupee-tree-928'
|
||||
'229','?','first-rupee-tree-birddash-1029'
|
||||
'230','?','first-rupee-tree-rta-x4FA-exit-921'
|
||||
'231','?','lumberjacks-652'
|
||||
'232','?','lumberjacks-picklearrow-y078-enter-455'
|
||||
'233','?','lumberjacks-rta-y078-enter-701'
|
||||
'234','?','lost-woods-01-1709'
|
||||
'235','?','lost-woods-01-y078-enter-1713'
|
||||
'236','?','master-sword-3909'
|
||||
'237','?','lost-woods-02-2252'
|
||||
'238','?','lost-woods-02-rta-2243'
|
||||
'239','?','fortune-teller-hut-rta-619'
|
||||
'240','?','whirlpool-pond-348'
|
||||
'241','?','wsb-543'
|
||||
'242','?','outside-hyrule-castle-1000'
|
||||
'243','?','castle-lobby-abovetorch-531'
|
||||
'244','?','castle-lobby-rta-540'
|
||||
'245','?','sw-room-455-bad-recording'
|
||||
'246','?','sw-room-456'
|
||||
'247','?','ramparts-617'
|
||||
'248','?','entrance-334'
|
||||
'249','?','goldknights-pui-734'
|
||||
'250','?','prizepack-guards-703'
|
||||
'251','?','first-dark-room-605'
|
||||
'252','?','first-dark-room-rta-619'
|
||||
'253','?','despair-1617-44'
|
||||
'254','?','last-3f-dark-room-436'
|
||||
'255','?','first-4f-dark-room-625'
|
||||
'256','?','first-4f-dark-room-pumpless-628'
|
||||
'257','?','melancholy-rta-715'
|
||||
'258','?','archerkey-beams-keydash-534'
|
||||
'259','?','archerkey-beams-rta-558'
|
||||
'260','?','redspearguards-809'
|
||||
'261','?','alert-guards-arrowkill-516'
|
||||
'262','?','alert-guards-beamkill-528'
|
||||
'263','?','cop-menu-638'
|
||||
'264','?','neopolitan-room-325'
|
||||
'265','?','statue-room-646'
|
||||
'266','?','catwalk-boom-624'
|
||||
'267','?','catwalk-boomless-621-bad-recording'
|
||||
'268','?','7f-hallway-521'
|
||||
'269','?','agah-cutscene-2451'
|
||||
'270','?','agah-0bb-4451'
|
||||
'271','?','pyramid-1043'
|
||||
'272','?','dark-octo-319'
|
||||
'273','?','hammer-bridge-444'
|
||||
'274','?','dark-east-fairy-cave-250'
|
||||
'275','?','outside-4157'
|
||||
'276','?','entrance1-dash-554'
|
||||
'277','?','entrance1-walk-556'
|
||||
'278','?','west-bomb-pickup-336'
|
||||
'279','?','shooter-key-847'
|
||||
'280','?','entrance2-333'
|
||||
'281','?','bomb-pickup1-428'
|
||||
'282','?','junction1-ledgehop-524'
|
||||
'283','?','junction1-slashjelly-556'
|
||||
'284','?','turtle-key-619'
|
||||
'285','?','entrance3-333'
|
||||
'286','?','bomb-pickup2-412'
|
||||
'287','?','junction2-907'
|
||||
'288','?','junction2-badrng-917'
|
||||
'289','?','upper-turtle-key-715'
|
||||
'290','?','bigkey-leftside-754'
|
||||
'291','?','entrance4-333'
|
||||
'292','?','bomb-pickup3-412'
|
||||
'293','?','junction3-651'
|
||||
'294','?','beetle-room1-954'
|
||||
'295','?','beetle-room1-dash2chest-1002'
|
||||
'296','?','beetle-room1-rta-1012'
|
||||
'297','?','hammeryump-1bombpickup-1324'
|
||||
'298','?','hammeryump-2bombpickup-1348'
|
||||
'299','?','darkmaze-738'
|
||||
'300','?','entrance5-333'
|
||||
'301','?','bomb-pickup4-412'
|
||||
'302','?','junction4-651'
|
||||
'303','?','beetle-room2-dash-and-hop-644'
|
||||
'304','?','beetle-room2-rta-walk-702'
|
||||
'305','?','beetle-room2-walk-and-hop-657'
|
||||
'306','?','sexy-statue-1333'
|
||||
'307','?','ugly-statue-1406'
|
||||
'308','?','mimics-454'
|
||||
'309','?','eye-statue-dashes-2454'
|
||||
'310','?','eye-statue-walk-rta-2456'
|
||||
'311','?','darkpegs-607'
|
||||
'312','?','lonely-turtle-457'
|
||||
'313','?','turtle-party-701-cropped'
|
||||
'314','?','turtle-party-701'
|
||||
'315','?','turtle-party-809'
|
||||
'316','?','warp-tile-512'
|
||||
'317','?','turtle-hallway-dash-and-keydash-709'
|
||||
'318','?','turtle-hallway-rta-walk-and-slash-745'
|
||||
'319','?','turtle-hallway-walk-and-keydash-720'
|
||||
'320','?','helma-2739'
|
||||
'321','?','helma-2813'
|
||||
'322','?','helma-2830'
|
||||
'323','?','helma-2843'
|
||||
'324','?','helma-2927'
|
||||
'325','?','outside-pod-2627'
|
||||
'326','?','dark-east-fairy-cave-259'
|
||||
'327','?','pegbridge-earlydash-639'
|
||||
'328','?','pegbridge-safedash-646'
|
||||
'329','?','bombshop-359'
|
||||
'330','?','annoying-bushes-402'
|
||||
'331','?','annoying-bushes-rightcancel-402'
|
||||
'332','?','southeast-of-fluteboy-dash-334'
|
||||
'333','?','southeast-of-fluteboy-walk-rta-328'
|
||||
'334','?','south-of-fluteboy-458'
|
||||
'335','?','south-of-fluteboy-walk-rta-508'
|
||||
'336','?','fluteboy-rta-1503'
|
||||
'337','?','grove-rta-1840'
|
||||
'338','?','south-of-grove-445'
|
||||
'339','?','grove-westside-236'
|
||||
'340','?','usain-bolt-720'
|
||||
'341','?','kak-village-centerdash-3335'
|
||||
'342','?','kak-village-leftdash-3335'
|
||||
'343','?','three-musketeers-3xdash-557'
|
||||
'344','?','lost-woods-830'
|
||||
'345','?','warp-rock-705'
|
||||
'346','?','warp-rock-no-hammerdash-724'
|
||||
'347','?','north-of-outcast-748'
|
||||
'348','?','voo-943'
|
||||
'349','?','sw-quad-badrng-745'
|
||||
'350','?','sw-quad-goodrng-736'
|
||||
'351','?','nw-quad-333'
|
||||
'352','?','ne-quad-badrng-350'
|
||||
'353','?','ne-quad-goodrng-337'
|
||||
'354','?','se-quad1-551'
|
||||
'355','?','bigkey-610'
|
||||
'356','?','se-quad2-628'
|
||||
'357','?','ne-quad2-733'
|
||||
'358','?','stalfos-hallway-hammer-755'
|
||||
'359','?','stalfos-hallway-pickup-754'
|
||||
'360','?','snek-and-zazaks-335'
|
||||
'361','?','conveyor-gibos-north-352'
|
||||
'362','?','conveyor-gibos-north-353'
|
||||
'363','?','hellway-429'
|
||||
'364','?','spikeroom1-hammer-515'
|
||||
'365','?','spikeroom1-mikestrat-502'
|
||||
'366','?','attic-pot-switch1-badrng-828'
|
||||
'367','?','attic-pot-switch1-goodrng-pickup-750'
|
||||
'368','?','grasshopper-hall1-1-252'
|
||||
'369','?','grasshopper-hall2-1-308'
|
||||
'370','?','attic-window-slashgrass-556'
|
||||
'371','?','grasshopper-hall2-2-240'
|
||||
'372','?','grasshopper-hall1-2-307'
|
||||
'373','?','attic-pot-switch2-306'
|
||||
'374','?','spikeroom2-606'
|
||||
'375','?','hellway2-322'
|
||||
'376','?','zazaks-and-gibos-543'
|
||||
'377','?','zazaks-and-gibos-545'
|
||||
'378','?','conveyor-toilet-bomb-404'
|
||||
'379','?','conveyor-toilet-nobomb-321'
|
||||
'380','?','bigblock-837'
|
||||
'381','?','lonely-zazak-315'
|
||||
'382','?','prison-dashes-1053'
|
||||
'383','?','prison-walk-1106'
|
||||
'384','?','prison-walk-oneslash-1124'
|
||||
'385','?','lonely-zazak2-252'
|
||||
'386','?','conveyor-jellies1-328'
|
||||
'387','?','bigchest-606'
|
||||
'388','?','conveyor-jellies2-509'
|
||||
'389','?','bigblock2-arrows-412'
|
||||
'390','?','bigblock2-noarrows-345'
|
||||
'391','?','conveyor-toilet2-dash-hammer-736'
|
||||
'392','?','conveyor-toilet2-hammerdash-726'
|
||||
'393','?','conveyor-toilet2-rta-751'
|
||||
'394','?','stalfos-hallway2-324'
|
||||
'395','?','blind-beams-2701'
|
||||
'396','?','blind-beams-walkin-2707'
|
||||
'397','?','voo1-902'
|
||||
'398','?','voo1-rta-914'
|
||||
'399','?','300-hut-721'
|
||||
'400','?','300-hut-rta-720'
|
||||
'401','?','voo2-dash-628'
|
||||
'402','?','voo2-walk-630'
|
||||
'403','?','cursed-dwarf-justpressa-1210'
|
||||
'404','?','cursed-dwarf-rta-1224'
|
||||
'405','?','voo3-339'
|
||||
'406','?','hammerpegs1-820'
|
||||
'407','?','outside-smiths1-350'
|
||||
'408','?','smiths1-708'
|
||||
'409','?','outside-smiths2-237'
|
||||
'410','?','smiths2-910'
|
||||
'411','?','outside-smiths3-239'
|
||||
'412','?','hammerpegs2-342'
|
||||
'413','?','outside-smiths4-338'
|
||||
'414','?','smiths3-746'
|
||||
'415','?','outside-smiths5-236'
|
||||
'416','?','hammerpegs3-849'
|
||||
'417','?','voo4-fencedash-441'
|
||||
'418','?','c-shaped-house-918'
|
||||
'419','?','voo5-709'
|
||||
'420','?','moblins-badrng-524'
|
||||
'421','?','moblins-goodrng-518'
|
||||
'422','?','skeleton-forest1-1216'
|
||||
'423','?','mummy-statue-1220'
|
||||
'424','?','bigkey-badrng-659'
|
||||
'425','?','bigkey-leftside-620'
|
||||
'426','?','bigkey-rightside-635'
|
||||
'427','?','mumm-statue-exit-232'
|
||||
'428','?','skeleton-forest2-949'
|
||||
'429','?','bigchest-earlybj-1259'
|
||||
'430','?','bigchest-rta-bonk-1407'
|
||||
'431','?','bigchest-rta-bonkless-1406'
|
||||
'432','?','skeleton-forest3-933'
|
||||
'433','?','mummy-statue-passthru-427'
|
||||
'434','?','bumper-hallway-305'
|
||||
'435','?','potkey-654'
|
||||
'436','?','skeleton-forest4-738'
|
||||
'437','?','skeleton-forest4-rta-740'
|
||||
'438','?','firesnake-keydash-522'
|
||||
'439','?','potpit-left-442'
|
||||
'440','?','potpit-right-438'
|
||||
'441','?','mummy-hellway-744'
|
||||
'442','?','mummy-hellway-joestrat-812'
|
||||
'443','?','vineroom-badrng-415'
|
||||
'444','?','vineroom-goodrng-359'
|
||||
'445','?','mummy-key-629'
|
||||
'446','?','mummy-key-rta-633'
|
||||
'447','?','mothhole-top-422'
|
||||
'448','?','mothula-1118'
|
||||
'449','?','mothula-1122'
|
||||
'450','?','outside-skull-menu-352'
|
||||
'451','?','lost-woods-dash-758'
|
||||
'452','?','lost-woods-walk-806'
|
||||
'453','?','links-house-632'
|
||||
'454','?','castle-gate-menu-rta-qw-637'
|
||||
'455','?','castle-gate-menu-walk-qw-639'
|
||||
'456','?','castle-gate-walk-qw-534'
|
||||
'457','?','pyramid-659'
|
||||
'458','?','dark-octofield-236'
|
||||
'459','?','broken-bridge-738'
|
||||
'460','?','lonely-ropa-243'
|
||||
'461','?','ropa-lottery-910'
|
||||
'462','?','dark-cuckoos-leftgrab-737'
|
||||
'463','?','dark-cuckoos-rta-746'
|
||||
'464','?','quake-1925'
|
||||
'465','?','quake-earlythrow-1858'
|
||||
'466','?','whirlpool1-dashcancel-605'
|
||||
'467','?','whirlpool1-nocancel-603'
|
||||
'468','?','zoras-domain-dashout-5934'
|
||||
'469','?','zoras-domain-walkout-5933'
|
||||
'470','?','tiny-warp-dik-605'
|
||||
'471','?','ice-warp-833'
|
||||
'472','?','ice-island-358'
|
||||
'473','?','ice-entrance-438'
|
||||
'474','?','ice2-508'
|
||||
'475','?','floor-switch-910'
|
||||
'476','?','block-intersection1-358'
|
||||
'477','?','compass-room-144'
|
||||
'478','?','block-intersection2-456'
|
||||
'479','?','penguins1-spin-535'
|
||||
'480','?','penguins1-tmstrat-523'
|
||||
'481','?','block-intersection3-512'
|
||||
'482','?','bombable-floor-dboost-916'
|
||||
'483','?','bombable-floor-nodboost-944'
|
||||
'484','?','bombable-floor-rta-good-rng-957'
|
||||
'485','?','big-stalfos-819'
|
||||
'486','?','conveyor-hellway-709'
|
||||
'487','?','ipbj-1209'
|
||||
'488','?','floor-zols-300'
|
||||
'489','?','penguins2-804'
|
||||
'490','?','big-spike-405'
|
||||
'491','?','lonely-firebar-419'
|
||||
'492','?','lonely-firebar-dash-451'
|
||||
'493','?','double-freezor-503'
|
||||
'494','?','big-chest-401'
|
||||
'495','?','big-pit-351'
|
||||
'496','?','block-push-hammerpot-badlag-542'
|
||||
'497','?','block-push-potpickup-badlag-539'
|
||||
'498','?','statue-room-hammer-both-pots-1323'
|
||||
'499','?','statue-room-hammer-one-pot-1306'
|
||||
'500','?','statue-room-pickup-both-pots-1318'
|
||||
'501','?','statue-room-pickup-one-pot-1304'
|
||||
'502','?','kholdstare-1poke-1621'
|
||||
'503','?','kholdstare-2poke-1559'
|
||||
'504','?','kholdstare-2poke-1602'
|
||||
'505','?','kholdstare-6slash-1655'
|
||||
'506','?','ice-island-343'
|
||||
'507','?','lake-hylia-birddash-803'
|
||||
'508','?','lake-hylia-walk-812'
|
||||
'509','?','links-house-521'
|
||||
'510','?','grassy-area-menu-437'
|
||||
'511','?','warp-nokill-422'
|
||||
'512','?','warp-octokill-437'
|
||||
'513','?','postwarp-hammerdash-950'
|
||||
'514','?','postwarp-walk-959'
|
||||
'515','?','outside-swamp1-noqw-629'
|
||||
'516','?','outside-swamp1-qw-617'
|
||||
'517','?','outside-watergate-noqw-427'
|
||||
'518','?','outside-watergate-qw-407'
|
||||
'519','?','watergate-entrance-mirrordash-455'
|
||||
'520','?','watergate-entrance-walk-500'
|
||||
'521','?','afskip-715'
|
||||
'522','?','afskip-bonk-736'
|
||||
'523','?','afskip-rta-732'
|
||||
'524','?','watergate-entrance2-317'
|
||||
'525','?','outside-watergate2-closewarp-236'
|
||||
'526','?','outside-watergate2-qw-413'
|
||||
'527','?','outside-swamp2-closewarp-424'
|
||||
'528','?','outside-swamp2-qw-352'
|
||||
'529','?','dungeon-entrance-1017'
|
||||
'530','?','potkey1-menu-1050'
|
||||
'531','?','potkey1-nomenu-923'
|
||||
'532','?','pool1-empty-627'
|
||||
'533','?','potkey2-dash-336'
|
||||
'534','?','potkey2-walk-339'
|
||||
'535','?','pool1-empty2-dash-515'
|
||||
'536','?','pool1-empty2-walk-514'
|
||||
'537','?','waterlever1-1232'
|
||||
'538','?','pool1-full-542'
|
||||
'539','?','bigchest1-839'
|
||||
'540','?','pool2-empty-710'
|
||||
'541','?','potkey3-dash-336'
|
||||
'542','?','potkey3-walk-339'
|
||||
'543','?','pool2-empty2-554'
|
||||
'544','?','bigchest2-759'
|
||||
'545','?','waterlever2-1355'
|
||||
'546','?','bigchest3-844'
|
||||
'547','?','pool2-full1-hammerdash-920'
|
||||
'548','?','pool2-full1-rta-939'
|
||||
'549','?','jelly-dash-1015'
|
||||
'550','?','sociable-firebar-1030'
|
||||
'551','?','jellydash-northside-433'
|
||||
'552','?','bigkey-719'
|
||||
'553','?','jelly-dash2-928'
|
||||
'554','?','pool2-full2-dboost-907'
|
||||
'555','?','pool2-full2-hammerdash-925'
|
||||
'556','?','pool2-full2-rta-936'
|
||||
'557','?','bigchest4-2026'
|
||||
'558','?','bigchest4-rta-2112'
|
||||
'559','?','statue-room-arrows-1205'
|
||||
'560','?','statue-room-no-arrows-1127'
|
||||
'561','?','red-jelly-2slash-351'
|
||||
'562','?','red-jelly-beam-340'
|
||||
'563','?','red-jelly-dash-326'
|
||||
'564','?','water-lever3-1146'
|
||||
'565','?','smallhall-313'
|
||||
'566','?','waterfall-room-509'
|
||||
'567','?','restock-all-448'
|
||||
'568','?','restock-arrows-only-422'
|
||||
'569','?','restock-bomb-only-420'
|
||||
'570','?','restock-skip-dash-330'
|
||||
'571','?','restock-skip-walk-334'
|
||||
'572','?','c-room-602'
|
||||
'573','?','phelps-way-943'
|
||||
'574','?','t-room-234'
|
||||
'575','?','arrghus-frqk-1737'
|
||||
'576','?','arrghus-frqk-1745'
|
||||
'577','?','arrghus-frqk-1748'
|
||||
'578','?','arrghus-noqk-1902'
|
||||
'579','?','arrghus-noqk-1930'
|
||||
'580','?','outside-swamp-357'
|
||||
'581','?','outside-swamp-from-fr-341'
|
||||
'582','?','outside-swamp-from-fr-with-qw-348'
|
||||
'583','?','outside-swamp-from-mirror-qw-236'
|
||||
'584','?','outside-watergate-852'
|
||||
'585','?','outside-watergate-from-qw-836'
|
||||
'586','?','dm-both-swordclimbs-2145'
|
||||
'587','?','dm-hookspeed-noswordclimbs-2202'
|
||||
'588','?','dm-hybrid-2205'
|
||||
'589','?','dm-spindash-2123'
|
||||
'590','?','dm-spindash-2134'
|
||||
'591','?','dm-spindash-dmgcancel-2111'
|
||||
'592','?','dark-dm-534'
|
||||
'593','?','ether-dashoff-2305'
|
||||
'594','?','ether-quickhop-2243'
|
||||
'595','?','octoballoon-610'
|
||||
'596','?','icerod-entrance-931'
|
||||
'597','?','icerod1-334'
|
||||
'598','?','icerodchest-nomenu-659'
|
||||
'599','?','icerodchest-rta-menu-815'
|
||||
'600','?','icerod2-menu-350'
|
||||
'601','?','icerod2-rta-nomenu-233'
|
||||
'602','?','icerod-leaving-547'
|
||||
'603','?','icerod-leaving-rta-556'
|
||||
'604','?','mirewarp-menu-701'
|
||||
'605','?','mire-entrance-nomenu-2501'
|
||||
'606','?','mire01-821'
|
||||
'607','?','mire02-1031'
|
||||
'608','?','mainhub1-toproute-947'
|
||||
'609','?','poporoom1-337'
|
||||
'610','?','spikekey1-351'
|
||||
'611','?','poporoom2-337'
|
||||
'612','?','beatthefireball-938'
|
||||
'613','?','jellykey-449'
|
||||
'614','?','tileroom-321'
|
||||
'615','?','bombslugs-508'
|
||||
'616','?','torches1-752'
|
||||
'617','?','torches2-rta-2127'
|
||||
'618','?','torches2-toplighting-2121'
|
||||
'619','?','bighole-322'
|
||||
'620','?','bigkey-621'
|
||||
'621','?','warptile-230'
|
||||
'622','?','wizzroom-412'
|
||||
'623','?','bigspike-318'
|
||||
'624','?','lonelystalfos-337'
|
||||
'625','?','sparkgamble-bestboost-1106'
|
||||
'626','?','sparkgamble-damageless-1144'
|
||||
'627','?','sparkgamble-rta-menu-1236'
|
||||
'628','?','sparkgamble-snekblock-1205'
|
||||
'629','?','maproom-south-327'
|
||||
'630','?','maproom-south-rta-327'
|
||||
'631','?','bigchest-menu-1012'
|
||||
'632','?','bigchest-nomenu-852'
|
||||
'633','?','maproom-north-328'
|
||||
'634','?','spikeykey2-rta-walk-702'
|
||||
'635','?','spikeykey2-rta-walk-magicgrab-753'
|
||||
'636','?','spikeykey2-rta-walk-magicgrab-with-hook-752'
|
||||
'637','?','jadinledge-hookdash-333'
|
||||
'638','?','jadinledge-quickhop-328'
|
||||
'639','?','jadinledge-rta-2dash-332'
|
||||
'640','?','wizzpot-rta-hook-610'
|
||||
'641','?','wizzpot-topdash-558'
|
||||
'642','?','bridge-504'
|
||||
'643','?','caneblockswitch-old-928'
|
||||
'644','?','caneblockswitch-spooky-934'
|
||||
'645','?','bigblock-429'
|
||||
'646','?','caneswitch-spooky-1132'
|
||||
'647','?','caneswitch-spooky-roddash-bonk-1108'
|
||||
'648','?','bombwall-spooky-dashout-513'
|
||||
'649','?','bombwall-spooky-dashout-risky-var-443'
|
||||
'650','?','bombwall-spooky-walk-534'
|
||||
'651','?','badfarmroom-spooky-dash-above-612'
|
||||
'652','?','badfarmroom-spooky-dash-below-608'
|
||||
'653','?','badfarmroom-spooky-riskier-walk-619'
|
||||
'654','?','badfarmroom-spooky-rta-walk-624'
|
||||
'655','?','firesnek-skip-menu-956'
|
||||
'656','?','firesnek-skip-rta-nomenu-909'
|
||||
'657','?','firesnek-skip-rta-nomenu-915'
|
||||
'658','?','vitty-nomenu-1732'
|
||||
'659','?','vitty-rta-menu-1846'
|
||||
'660','?','?'
|
||||
'661','?','outside-mire-335'
|
||||
'662','?','outside-desert-818'
|
||||
'663','?','wafflehouse-drivethru-1241'
|
||||
'664','?','wafflehouse-drivethru-nostairclimb-1243'
|
||||
'665','?','broken-bridge-1034'
|
||||
'666','?','broken-bridge-fromfacingup-1037'
|
||||
'667','?','paradox-lower-454'
|
||||
'668','?','paradox-upper-740'
|
||||
'669','?','run-killing-deadrocks-525'
|
||||
'670','?','hammerpegs-1251'
|
||||
'671','?','outside-trock-2057'
|
||||
'672','?','outside-trock-rta-1957'
|
||||
'673','?','dungeon-entrance-menu-933'
|
||||
'674','?','dungeon-entrance-no-menu-818'
|
||||
'675','?','largepit1-928'
|
||||
'676','?','torches-1428'
|
||||
'677','?','torches-rta-1432'
|
||||
'678','?','rollerroom-1038'
|
||||
'679','?','rollerroom-topdmg-1032'
|
||||
'680','?','torches-backtrack-346'
|
||||
'681','?','largepit2-1025'
|
||||
'682','?','pokey0-526'
|
||||
'683','?','pokey0-rta-551'
|
||||
'684','?','chomps-2blocks-805'
|
||||
'685','?','chomps-bbb-758'
|
||||
'686','?','chomps-blocknbeam-748'
|
||||
'687','?','tunnels-arrows-2040'
|
||||
'688','?','tunnels-no-arrows-1948'
|
||||
'689','?','lava-room1-739'
|
||||
'690','?','pokey1-bl-keydash-849'
|
||||
'691','?','bigkey-fromdash-1245'
|
||||
'692','?','bigkey-fromwalk-1241'
|
||||
'693','?','pokey1-backtrack-beamless-644'
|
||||
'694','?','pokey1-backtrack-beams-632'
|
||||
'695','?','pokey1-backtrack-canedash-624'
|
||||
'696','?','lava-room3-1247'
|
||||
'697','?','double-pokeys-429'
|
||||
'698','?','miniroller-2dash-339'
|
||||
'699','?','lavaroom4-619'
|
||||
'700','?','walldash-414'
|
||||
'701','?','crystalroller-beams-812'
|
||||
'702','?','darkroom-2428'
|
||||
'703','?','helmadash1-623'
|
||||
'704','?','laserskip-block-921'
|
||||
'705','?','laserskip-dash-823'
|
||||
'706','?','helmadash2-414'
|
||||
'707','?','helmadash2-rta-417'
|
||||
'708','?','canedash-725[30lf]'
|
||||
'709','?','canedash-725[31lf]'
|
||||
'710','?','canedash-magicless-814'
|
||||
'711','?','canedash-rta-walk-823'
|
||||
'712','?','restock-graball-1238'
|
||||
'713','?','restock-magicgrab-1232'
|
||||
'714','?','restock-skipall-1145'
|
||||
'715','?','trinexx-rta-3759'
|
||||
'716','?','outside-trock-454'
|
||||
'717','?','lynel-bridge-909'
|
||||
'718','?','entrance-2239'
|
||||
'719','?','entrance-nostairclimb-2251'
|
||||
'720','?','foyer-624'
|
||||
'721','?','torchkey-bonkandwalk-759'
|
||||
'722','?','torchkey-hook-758'
|
||||
'723','?','torchkey-nokey-606'
|
||||
'724','?','conveyor-potkey-905'
|
||||
'725','?','spikeskip-919'
|
||||
'726','?','spikeskip-noskip-937'
|
||||
'727','?','double-crystal-switch-719'
|
||||
'728','?','double-crystal-switch-rta-739'
|
||||
'729','?','double-crystal-switch-rta-740'
|
||||
'730','?','spike-and-pegs-356'
|
||||
'731','?','firesnake-room-1029'
|
||||
'732','?','firesnake-room-bonk-1117'
|
||||
'733','?','firesnake-room-framerule-1011'
|
||||
'734','?','warp-tile1-157'
|
||||
'735','?','warp-tile2-344'
|
||||
'736','?','warp-tile2-justholddown-350'
|
||||
'737','?','warp-tile3-430'
|
||||
'738','?','warp-tileskip-816'
|
||||
'739','?','warp-tileskip-with-menu-931'
|
||||
'740','?','false-floor-no-menu-1017'
|
||||
'741','?','false-floor-with-menu-1122'
|
||||
'742','?','bombable-floor-717'
|
||||
'743','?','icearmos-1529'
|
||||
'744','?','icearmos-321-1548'
|
||||
'745','?','icearmos-321-dboost-1546'
|
||||
'746','?','bk-arrows-738'
|
||||
'747','?','bk-no-arrows-606'
|
||||
'748','?','foyer2-745'
|
||||
'749','?','mj-room-1045-arrow-dash'
|
||||
'750','?','mimics1-joestrat-647'
|
||||
'751','?','mimics1-myramics-653'
|
||||
'752','?','mimics2-blunt-553'
|
||||
'753','?','mimics2-opt-519'
|
||||
'754','?','east-spike-room-302'
|
||||
'755','?','spiketrap-1341'
|
||||
'756','?','spiketrap-abovepot-1406'
|
||||
'757','?','gbz-513'
|
||||
'758','?','gbz-walk-637'
|
||||
'759','?','g1-838'
|
||||
'760','?','g2-441'
|
||||
'761','?','g3-644'
|
||||
'762','?','g4-446'
|
||||
'763','?','g5-459'
|
||||
'764','?','bunnybeamhall-305'
|
||||
'765','?','lanmo2-1141'
|
||||
'766','?','restock-248'
|
||||
'767','?','wizz1-849'
|
||||
'768','?','guardbridge-529'
|
||||
'769','?','wizz2-429'
|
||||
'770','?','foosdabridge-653'
|
||||
'771','?','foosdabridge-rightdash-703'
|
||||
'772','?','torches1-1251-magic-noroddash'
|
||||
'773','?','eyelasers-529'
|
||||
'774','?','torches2-637'
|
||||
'775','?','torches2-safewalk-647'
|
||||
'776','?','helmakey-500-dmg'
|
||||
'777','?','helmakey-507-dmgless'
|
||||
'778','?','bombwall-521'
|
||||
'779','?','bombwall-damageless-530'
|
||||
'780','?','bombwall-rta-543'
|
||||
'781','?','pegschestkey-843'
|
||||
'782','?','pegschestkey-magicskip-454'
|
||||
'783','?','pegschestkey-safe-634'
|
||||
'784','?','mold2-hover-1232'
|
||||
'785','?','mold2-hover-1301'
|
||||
'786','?','mold2-hover-1337'
|
||||
'787','?','mold2-kill-hook-1949'
|
||||
'788','?','mold2-kill-hook-1958'
|
||||
'789','?','helma-hallway-607'
|
||||
'790','?','helma-hallway-safe-620'
|
||||
'791','?','torch-hallway-635'
|
||||
'792','?','agah2-4927'
|
||||
'793','?','?'
|
||||
'794','?','?'
|
||||
'795','?','?'
|
||||
'796','?','?'
|
||||
'797','?','?'
|
||||
'798','?','ganon-139.55-nfc'
|
||||
'799','?','ganon-139.55'
|
||||
'800','?','?'
|
||||
'801','?','?'
|
||||
'802','?','?'
|
||||
'803','?','?'
|
||||
'804','?','?'
|
||||
'805','?','?'
|
||||
'806','?','?'
|
||||
'807','?','?'
|
||||
'808','?','?'
|
||||
'809','?','?'
|
||||
'810','?','?'
|
||||
'811','?','?'
|
||||
'812','?','?'
|
||||
'813','?','?'
|
||||
'814','?','?'
|
||||
'815','?','?'
|
||||
'816','?','?'
|
||||
47
seed.example.json
Normal file
47
seed.example.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"botName": "greenhambot",
|
||||
"discord": {
|
||||
"clientId": "YOUR DISCORD APP ID",
|
||||
"token": "YOUR DISCORD APP TOKEN",
|
||||
"adminUserId": "YOUR DISCORD USER ID",
|
||||
"guilds": [
|
||||
{
|
||||
"internalName": "GUILD NAME",
|
||||
"id": "GUILD ID",
|
||||
"prefix": "!",
|
||||
"enableSfx": true,
|
||||
"sfxVolume": 0.5,
|
||||
"passes": 2,
|
||||
"allowedRolesForRequest": ["DISCORD ROLE ID"],
|
||||
"enableFunFacts": true,
|
||||
"enableHamFacts": true,
|
||||
"scheduledEvents": [
|
||||
{
|
||||
"id": "rise-up-and-kick-a-little-ass",
|
||||
"schedule": {
|
||||
"hour": 7,
|
||||
"minute": 30,
|
||||
"tz": "America/Los_Angeles"
|
||||
},
|
||||
"channelId": "DISCORD CHANNEL ID",
|
||||
"pingRoleId": "DISCORD ROLE ID",
|
||||
"message": "I'm gonna rise up, I'm gonna kick a little ass, Gonna kick some ass in the USA, Gonna climb a mountain, Gonna sew a flag, Gonna fly on an Eagle, I'm gonna kick some butt, I'm gonna drive a big truck, I'm gonna rule this world, Gonna kick some ass, Gonna rise up, Kick a little ass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"internalName": "SECOND GUILD NAME",
|
||||
"id": "SECOND GUILD ID",
|
||||
"prefix": "!",
|
||||
"enableSfx": true,
|
||||
"sfxVolume": 0.5,
|
||||
"passes": 2,
|
||||
"enableFunFacts": true,
|
||||
"enableHamFacts": true
|
||||
}
|
||||
],
|
||||
"activities": ["Chardee MacDennis", "The Nightman Cometh", "Charlie Work"],
|
||||
"blacklistedUsers": ["IGNORE COMMANDS FROM THESE DISCORD USER IDS"]
|
||||
},
|
||||
"debug": false
|
||||
}
|
||||
BIN
sfx/1v2.mp3
BIN
sfx/1v2.mp3
Binary file not shown.
BIN
sfx/2+2.mp3
Normal file → Executable file
BIN
sfx/2+2.mp3
Normal file → Executable file
Binary file not shown.
BIN
sfx/20gp.mp3
Executable file
BIN
sfx/20gp.mp3
Executable file
Binary file not shown.
17
sfx/README.md
Normal file
17
sfx/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
**🔉 GENERAL**
|
||||
|
||||
```
|
||||
2+2, ahhh, alert, aspen, auw, aww, bonk, bustin, cd, chafe, chipotle, chomp, choochoo, cooler,correct, date, dong, duck, enjoy, ez, fakehands, fbrage, fine, flippers, funnyhow, gatekeepah, gcn, groovy, gyst, help, herewego, heyheyhey, heymf, highscore, hop, how, hype, idgaf, imawot, interesting, jacked, knob, lab, laugh, lisa, long, mad, massage, mayo, meme, mmmm, mouthfeel, mybody, neat, nevergiveup, obaeb, ohno, okusa, onejoint, onfire, ow, poopy, popup, porkchop, pour, ppump, qty, raffle, rawr, rentfree, respect, robotears, rpgfarm, sdgtw, sendit, slowmo-in, slowmo-out, sofast, sogood, stick, store, suh, swag, tasty, tea, thatthing, theline, tmm, tojesus, tootski, trash, triple, urf, wahwah, wanker, waow, wdied, wow, yahoo, yippee, yoshi, youguys
|
||||
```
|
||||
|
||||
**🤓 NERDS**
|
||||
|
||||
```
|
||||
1v2, airplane, anders, anteater, archery, blazeit, blblbl, booty, bossmusic, butt, bwaa, bye, byeb, craft, dbio, deebsfart, deebslaugh, diaper, disagree, dominos, doomtaonline, duckscream, eggs, emetarage, emetarage2, emetarage3, emmapos, english, f, fdup, fencedash, fk, fu, fuckduck, fuzzy, goofy, h2o, hamhelp, hamlaugh, hamlaugh2, hamscream, hellway, jebaited, joshbitch, joshgoat, joshlaugh, joshlaugh2, joshlaugh3, joshlaugh4, joshscream, joshwhinny, joshx, lanmo, lanx, lanx2, lol, mcgasm, milk, mothhole, muttfart, muttfart2, muttlaugh, muttscream, muttwalksin, myman, ncd, nfc, oh, pprage, ppscream, pyle, rando, rip, run, runsover, sgqf, shj, smd, spam, speednoises, speedrunner, split, stank, stfu, tbhamfact, teats, timmon, trinexx, vitty, wait, whathaveidone, why, wiki, wobbuffet, xelboss, xelkiss
|
||||
```
|
||||
|
||||
**🎥 TV & MOVIES**
|
||||
|
||||
```
|
||||
20gp, albert, algore, andhot, arise, banana, bananas, bigshit, bill, bjqueen, carlcandy, clever, coffee, crush, cumin, dd, dead, digup, dinodna, dodson, dontmatter, door, drinkfull, esp, fish, goldcaps, gordon, gordon2, gordon3, gordon4, gotalight, gross, gum, hahaa, hammertime, hatethatman, hightonight, hrwah, human, hunt, idc, kudos, lang, learnding, letsrock, mapleham, mash, mrjackpots, myarms, nerd, newshoes, oldman, party, please, present, puzzles, pushpush, rake, rockin, seaparks, seeitnow, shirtballs, stahp, threat, tonight, toofat, tour, towel, trex, unhelpful, veranda, vision, watchyourback
|
||||
```
|
||||
BIN
sfx/ahhh.mp3
Normal file → Executable file
BIN
sfx/ahhh.mp3
Normal file → Executable file
Binary file not shown.
BIN
sfx/airplane.mp3
BIN
sfx/airplane.mp3
Binary file not shown.
BIN
sfx/albert.mp3
Executable file
BIN
sfx/albert.mp3
Executable file
Binary file not shown.
BIN
sfx/alert.mp3
Executable file
BIN
sfx/alert.mp3
Executable file
Binary file not shown.
BIN
sfx/algore.mp3
Normal file
BIN
sfx/algore.mp3
Normal file
Binary file not shown.
BIN
sfx/anders.mp3
Executable file
BIN
sfx/anders.mp3
Executable file
Binary file not shown.
BIN
sfx/andhot.mp3
Executable file
BIN
sfx/andhot.mp3
Executable file
Binary file not shown.
Binary file not shown.
BIN
sfx/anteater.mp3
Normal file
BIN
sfx/anteater.mp3
Normal file
Binary file not shown.
BIN
sfx/archery.mp3
BIN
sfx/archery.mp3
Binary file not shown.
BIN
sfx/arise.mp3
BIN
sfx/arise.mp3
Binary file not shown.
BIN
sfx/aspen.mp3
Normal file → Executable file
BIN
sfx/aspen.mp3
Normal file → Executable file
Binary file not shown.
BIN
sfx/auw.mp3
BIN
sfx/auw.mp3
Binary file not shown.
BIN
sfx/aww.mp3
BIN
sfx/aww.mp3
Binary file not shown.
BIN
sfx/banana.mp3
Normal file
BIN
sfx/banana.mp3
Normal file
Binary file not shown.
BIN
sfx/bananas.mp3
BIN
sfx/bananas.mp3
Binary file not shown.
BIN
sfx/bigshit.mp3
Executable file
BIN
sfx/bigshit.mp3
Executable file
Binary file not shown.
BIN
sfx/bill.mp3
Normal file
BIN
sfx/bill.mp3
Normal file
Binary file not shown.
BIN
sfx/bjqueen.mp3
Executable file
BIN
sfx/bjqueen.mp3
Executable file
Binary file not shown.
BIN
sfx/blazeit.mp3
Normal file → Executable file
BIN
sfx/blazeit.mp3
Normal file → Executable file
Binary file not shown.
BIN
sfx/blblbl.mp3
Executable file
BIN
sfx/blblbl.mp3
Executable file
Binary file not shown.
BIN
sfx/bonk.mp3
Executable file
BIN
sfx/bonk.mp3
Executable file
Binary file not shown.
BIN
sfx/booty.mp3
Executable file
BIN
sfx/booty.mp3
Executable file
Binary file not shown.
Binary file not shown.
BIN
sfx/bustin.mp3
BIN
sfx/bustin.mp3
Binary file not shown.
BIN
sfx/butt.mp3
Normal file → Executable file
BIN
sfx/butt.mp3
Normal file → Executable file
Binary file not shown.
BIN
sfx/butwait.mp3
BIN
sfx/butwait.mp3
Binary file not shown.
BIN
sfx/bwaa.mp3
Normal file → Executable file
BIN
sfx/bwaa.mp3
Normal file → Executable file
Binary file not shown.
BIN
sfx/bye.mp3
BIN
sfx/bye.mp3
Binary file not shown.
BIN
sfx/byeb.mp3
Executable file
BIN
sfx/byeb.mp3
Executable file
Binary file not shown.
Binary file not shown.
BIN
sfx/cd.mp3
BIN
sfx/cd.mp3
Binary file not shown.
BIN
sfx/chafe.mp3
Executable file
BIN
sfx/chafe.mp3
Executable file
Binary file not shown.
BIN
sfx/chipotle.mp3
Normal file → Executable file
BIN
sfx/chipotle.mp3
Normal file → Executable file
Binary file not shown.
BIN
sfx/chomp.mp3
Normal file → Executable file
BIN
sfx/chomp.mp3
Normal file → Executable file
Binary file not shown.
BIN
sfx/choochoo.mp3
BIN
sfx/choochoo.mp3
Binary file not shown.
Binary file not shown.
BIN
sfx/clever.mp3
Executable file
BIN
sfx/clever.mp3
Executable file
Binary file not shown.
BIN
sfx/coffee.mp3
Executable file
BIN
sfx/coffee.mp3
Executable file
Binary file not shown.
Binary file not shown.
BIN
sfx/correct.mp3
Executable file
BIN
sfx/correct.mp3
Executable file
Binary file not shown.
BIN
sfx/craft.mp3
Normal file
BIN
sfx/craft.mp3
Normal file
Binary file not shown.
BIN
sfx/crush.mp3
Normal file
BIN
sfx/crush.mp3
Normal file
Binary file not shown.
BIN
sfx/cumin.mp3
Normal file
BIN
sfx/cumin.mp3
Normal file
Binary file not shown.
BIN
sfx/date.mp3
Normal file
BIN
sfx/date.mp3
Normal file
Binary file not shown.
BIN
sfx/dbio.mp3
BIN
sfx/dbio.mp3
Binary file not shown.
BIN
sfx/dd.mp3
Normal file
BIN
sfx/dd.mp3
Normal file
Binary file not shown.
BIN
sfx/dead.mp3
Executable file
BIN
sfx/dead.mp3
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
sfx/diaper.mp3
BIN
sfx/diaper.mp3
Binary file not shown.
BIN
sfx/dickhole.mp3
BIN
sfx/dickhole.mp3
Binary file not shown.
BIN
sfx/digup.mp3
Executable file
BIN
sfx/digup.mp3
Executable file
Binary file not shown.
BIN
sfx/dinodna.mp3
Executable file
BIN
sfx/dinodna.mp3
Executable file
Binary file not shown.
BIN
sfx/disagree.mp3
BIN
sfx/disagree.mp3
Binary file not shown.
BIN
sfx/dode.mp3
BIN
sfx/dode.mp3
Binary file not shown.
BIN
sfx/dodson.mp3
Executable file
BIN
sfx/dodson.mp3
Executable file
Binary file not shown.
BIN
sfx/dominos.mp3
Normal file
BIN
sfx/dominos.mp3
Normal file
Binary file not shown.
BIN
sfx/dong.mp3
Executable file
BIN
sfx/dong.mp3
Executable file
Binary file not shown.
Binary file not shown.
BIN
sfx/doomtaonline.mp3
Normal file
BIN
sfx/doomtaonline.mp3
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user