Jun 19

RetroPie Install Guide

This original guide is meant for the Raspberry PI 2/3/3B+ (Updated 11/5/2020)

*** Click the below link to see the updated version for the RaspberryPi 4B! ***
http://sigkillit.com/2020/09/09/retropie-install-guide-raspberrypi-4/

Hardware Used

You can easily go cheaper on the hardware by getting USB controllers, getting a smaller MicroSD card, not adding the optional fan, or by not getting the optional hardware in favor of using existing USB keyboards and existing PS/Xbox controllers. However, at minimum I would recommend the RaspberryPi v3 Model B+ and the Retroflag case!

  • CanaKit RaspberryPi Version 3 Model B+, heatsink kit, and 2.5A Power Supply
  • SanDisk Ultra 128GB microSDXC UHS-I Card with Adapter, Black, Standard Packaging (SDSQUNC-128G-GN6MA)
  • Retroflag NESPI Case Plus
    • $24 on Amazon
    • This is an exact mini replica of the original NES with function power & reset buttons. They also make other replica game system cases, visit http://www.retroflag.com for the full list
  • Optional: Pi-Fan
  • Optional: 2 x 8Bitdo Sn30 Bluetooth Gamepad
  • Optional: Rii mini i8+ Keyboard
    • $20 on Amazon
    • Note: You can use an existing USB or USB Wireless keyboard

Software Used

Resources

Update 8Bitdo Controller Firmware

  • Download the latest 8Bitdo Controller Firmware on a Windows Computer
    1. http://support.8bitdo.com
  • Run the update utility and follow the onscreen instructions

Create RetroPie SD Card

  • Download RetroPie imager/installer for the appropriate version of your Raspberry Pi on a Windows Computer
  • Connect the MicroSD card to your Windows Computer
  • Open the downloaded Raspberry Pi Imager software
    • It will automatically select the Operating System for downloaded version of RetroPi
    • Select your MicroSD card as the Storage device
    • Click Write to write RetroPi to the MicroSD card

Install the RaspberryPi Into the Case

  • Make sure the case is powered off
  • Attach the heatsinks onto the RaspberryPi
  • Install the RaspberryPi into the Retroflag case as per the directions
  • Switch the Safe Shutdown switch to ON
  • Connect the HDMI cable, Ethernet Cable, keyboard USB dongle, insert the microSD card, and insert the micro-USB power cable

Configure US Keyboard on First Boot

  • Make sure the keyboard dongle is connected and the keyboard is charged and powered on
  • Turn the power to “ON” on the case
  • On the Welcome Screen, hold any button on the keyboard to configure it, and configure it as follows:
    • D-PAD UP: Up Arrow
    • D-PAD DOWN: Down Arrow
    • D-PAD LEFT: Left Arrow
    • D-PAD RIGHT: Right Arrow
    • START: 1
    • SELECT: 2
    • A: Enter (Same as OK key)
    • B: Escape
    • For all other keys, hold any button to skip configuring
    • When OK is highlighted, press the OK button
    • When prompted about not enabling a hotkey button, highlight YES and press OK to use the default select button
  • Navigate to the RetroPie menu and press OK on the keyboard
    • Select RASPI-CONFIG and press OK on the keyboard
      • Select Localization Options and press Enter on the keyboard
      • Select Keyboard and press Enter on the keyboard
      • Select Generic 105-key (Intl) PC and press Enter on the keyboard
      • For the keyboard layout, select Other and press Enter on the keyboard
        1. Select Other and press Enter on the keyboard
        2. Select English (US) and press Enter on the keyboard
      • Select English (US) and press Enter on the keyboard
      • Select The default for the keyboard layout and press Enter on the keyboard
      • Select No Composer Key and press Enter on the keyboard
      • Select Finish and press Enter on the keyboard

Optional – Connect to WiFi (Skip if using wired LAN)

  • Navigate to the RetroPie menu and press OK on the keyboard
  • Select RASPI-CONFIG and press OK on the keyboard
    • Select Localization Options and press OK on the keyboard
    • Select Change WI-FI Country and press OK on the keyboard
    • Select United States and press OK on the keyboard
    • Press OK on the success message
    • Select Finish and press OK on the keyboard
  • *Select WIFI and press OK on the keyboard
    • Select Connect to WIFI network and press OK on the keyboard
    • Select the SSID, and press OK on the keyboard
    • Enter the SSID PSIK and press Enter on the keyboard
    • Select Exit and press Enter on the keyboard

Optional – Enable SSH (Useful later for scraping ROMs)

  • Navigate to the RetroPie menu and press Enter on the keyboard
  • Select RASPI-CONFIG and press Enter on the keyboard
  • Select Interface Options and press Enter on the keyboard
    • Select SSH and press Enter on the keyboard
    • Select Yes to enable SSH and press Enter on the keyboard
    • Press OK to confirm SSH is enabled
    • Select Finish and press Enter on the keyboard

Optional – Update RetroPie

  • Navigate to the RetroPie menu and press OK on the keyboard
  • Select RETROPIE SETUP and press OK on the keyboard
    • Select Update and press Enter on the keyboard
      • When prompted, select Yes and press Enter on the keyboard
      • Press OK/YES to all proceeding messages
      • Select Exit and press Enter on the keyboard
  • If RetroPie Does Not Boot Emulation Station and is stuck asking for the RetroPie login
    • Login as pi
    • Navigate to the RetroPie menu and press OK on the keyboard
    • Select RASPI-CONFIG and press OK on the keyboard
    • Select Boot Options and press OK on the keyboard
    • Select Desktop/CLI and press OK on the keyboard
    • Select Console Autologin and press OK on the keyboard
    • Select Finish and press OK on the keyboard
    • Select Yes to reboot and press OK on the keyboard
    • It should restart and autologin to emulationstation again

Add the Retroflag Safe Shutdown Script

Retroflag Case Power LED & Buttons Will NOT Work Until This is Installed

I personally recommend using crcerror’s forked version of the retroflag script vs. the official retroflag version of the script for the power/reset buttons. Here are the differences and instructions to install either:

  • crcerror Script (Forked Version of Retroflag’s)
    • Button Functions:
      • Press Reset in Emulator: Exits emulator back to Emulationstation
      • Press Reset in Emulationstation: Restarts Emulationstation
      • Press Reset in Console: Restart the RaspberryPi
      • Press Power in Console, Emulationstation, or ROM Emulator: Shutdown the RaspberryPi
    • Install Directions:
      • Ensure you are connected to the internet & the keyboard is connected
      • Press F4 to enter the terminal
      • In the terminal, type the one-line command below (Case Sensitive):
        • wget -O – “https://raw.githubusercontent.com/crcerror/retroflag-picase/master/install.sh” | sudo bash
      • RetroPie will restart once complete
  • Retroflag Script
    • Button Functions:
      • Press Reset in Emulator/Emulationstation: Restart the RaspberryPi
      • Press Reset in Console: Nothing
      • Press Power in Emulator/Emulationstation: Shutdown the RaspberryPi
      • Press Power in Console: Nothing
    • Install Directions:
      • Ensure you are connected to the internet & the keyboard is connected
      • Press F4 to enter the terminal
      • In the terminal, type the one-line command below (Case Sensitive):
        • wget -O – “https://raw.githubusercontent.com/RetroFlag/retroflag-picase/master/install.sh” | sudo bash
      • RetroPie will restart once complete

Setup Bluetooth Controllers

This was written for the 8Bitdo controllers, but should be the same for any Bluetooth controller

  • Navigate to the RetroPie menu and press OK on the keyboard
  • Select BLUETOOTH and press OK on the keyboard
    • Make sure 8Bitdo mapping hack is set to off
    • Power on the SNES30 controller by holding the start button until the blue light comes on.  Once it is blinking, set it to pairing mode by holding the select button for 3 seconds and the blue light will start rapidly blinking
    • Select Pair and Connect to Bluetooth Device and press Enter on the keyboard
    • The controller will show up with its MAC address as 8Bitdo, select it and press Enter on the keyboard
    • Select DisplayYesNo and press Enter on the keyboard
      • Repeat for each additional controller
    • Select Setup udev rule for joypad and press Enter on the keyboard
      • Select an 8Bitdo controller and press Enter on the keyboard
      • Press Enter on the keyboard to confirm the entry was added
        • Repeat for each additional controller
    • Select Cancel and press Enter on the keyboard to return to Emulationstation
  • Press 1 on the keyboard to bring up the Main Menu
    • Select Configure Input and press OK on the keyboard
    • When prompted to configure input, press OK on the keyboard
    • Hold any key on the 8Bitdo SNES30 controller and configure it as follows
    • D-PAD UP: Up Arrow
    • D-PAD DOWN: Down Arrow
    • D-PAD LEFT: Left Arrow
    • D-PAD RIGHT: Right Arrow
    • START: Start
    • SELECT: Select
    • A: A
    • B: B
    • X: X
    • Y: Y
    • LEFT SHOULDER: L
    • RIGHT SHOULDER: R
    • For all others, hold any button to skip configuration (Or use the keyboard to skip down to OK)
    • When OK is highlighted, press the OK on the keyboard
    • When prompted about not enabling a hotkey button, highlight YES and press OK on the keyboard to use the default select button
    • Repeat for each Controller
  • Press 1 on the keyboard to bring up the Main Menu
    • Select Quit and press OK on the keyboard
    • Select Restart System and press OK on the keyboard
    • Select Yes to confirm the restart and press OK on the keyboard

Install Optional Emulators

SYSTEM OPTIONAL EMULATOR NOTE
Apple II linapple  
Atari ST hatari  
Commodor 64 vice  
Coleco Vision coolcv  
Dragon 32 / Tandy CoCo xroar  
Intellivision lr-freeintv  
MSX lr-fmsx Adds the following files to BIOS folder: CARTS.SHA, CYRILLIC.FNT, DISK.ROM, FMPAC.ROM, FMPAC16.ROM, ITALIC.FNT, KANJI.ROM, MSX.ROM, MSX2.ROM, MSX2EXT.ROM, MSX2P.ROM, MSX2PEXT.ROM, MSXDOS2.ROM, PAINTER.ROM, RS232.ROM
Virtual Boy lr-beetle-vb  
Wonderswan / Wonderswan Color lr-beetle-wswan  
Zmachine frotz Includes Zork 1, 2, and 3
  • Navigate to the RetroPie menu and press OK on the keyboard
    • Select RETROPIE SETUP and press OK on the keyboard
    • Select Manage packages and press enter on the keyboard
    • Select Manage Optional packages and press enter on the keyboard
    • Select %EMULATOR_NAME% and press enter on the keyboard
    • Select Install from source (latest) or Install from binary and press enter on the keyboard and wait for the installer to finish
      • Select Back and press enter on the keyboard
      • Optionally, repeat the process to install more emulators or to Exit
        • Select Back and press enter on the keyboard
        • Select Back and press enter on the keyboard
        • Select Exit and press enter on the keyboard

Install Experimental Emulators

SYSTEM EXPERIMENTAL EMULATOR
Nintendo DS drastic
  • Navigate to the RetroPie menu and press OK on the keyboard
    • Select RETROPIE SETUP and press OK on the keyboard
    • Select Manage packages and press enter on the keyboard
    • Select Manage Experimental packages and press enter on the keyboard
    • Select %EMULATOR_NAME% and press enter on the keyboard
    • Select Install from source (latest) or Install from binary and press enter on the keyboard and wait for the installer to finish
      • Select Back and press enter on the keyboard
      • Optionally, repeat the process to install more emulators or to Exit
        • Select Back and press enter on the keyboard
        • Select Back and press enter on the keyboard
        • Select Exit and press enter on the keyboard

Add BIOS, Config, and ROM Files to the SD Card

NOTE: See my post on cleaning ROM collections:
http://sigkillit.com/2019/06/19/clean-no-intro-2018-rom-set/

  • From a Windows computer containing the BIOS and Config Files, open the samba share for the RetroPie by entering \\retropie
    • If this does not work, navigate to the RetroPie menu, press Enter on the keyboard, select Show IP, press OK on the keyboard and note the IP then press Enter on the keyboard
    • Try to access the samba share for the RetroPie by using the IP address noted above: \\IPADDRESS
  • Copy the BIOS files to the BIOS Share
  • Copy your ROMs to the ROMS share
  • OPTIONAL – Copy Configs to the CONFIGS share
    • You only need to do this if you have previous configurations backed up & do not want to treat this as a brand new install

Special Configurations for ROMs to Work Properly

Atari 800 / Atari 5200 Load BIOS

You need to configure the emulator to point it to the correct BIOS files for these to work

  • Launch an Atari 800/5200 game
  • If you launched a 5200 game, select Standard 5200 cartridge option or if you launched a 800 game you won’t see this prompt just continue.  You’ll get a warning that you need a real Atari/OS.
  • Press F1 on the keyboard to open the menu
  • Select Emulator Configuration and press enter on the keyboard
  • Select System ROM Settings and press enter on the keyboard
  • Select Find ROM images in a directory, navigate to the BIOS directory (/home/pi/RetroPie/BIOS) and press the space bar on the keyboard to select that directory.
  • Press Escape on the keyboard to go back a menu
  • Select Save Configuration File and press enter on the keyboard
  • Select Save Configuration on Exit and press enter on the keyboard
  • Press Escape on the keyboard to go back a menu
  • Select Exit Emulator and press enter on the keyboard
  • If you have the correctly named BIOS files and it should automatically configure them

Atari 5200 Auto Load Game (Prevents Memo Pad From Loading)

If the screen loads to a blue memo pad instead of loading the game, this is because it’s set to an 800 system.  You can press F1 and load the rom fine each time or you can do the following to override it

  • Launch the Atari 5200 game
  • Press the Select + X (Hotkey + X) to bring up the Quick Menu
    • Select Options and press enter on the keyboard
    • Select Atari System and press the right arrow to select 5200
    • Press B to return to the previous menu
    • Select Resume and press A to return to the game
    • Test to see if it works now.  If it works, you can permanently save it by going back to Controls and selecting Save Core Overrides

Atari 800 / Atari 5200 Configure Controller

Update: Detects “RetroPad” controllers as of version lr-atari800 version 1.9.7

  • Launch Atari 800/5200 game
  • Press the Select + X (Hotkey + X) to bring up the Quick Menu
    • Select Controls and press A
    • Next to user 1 Device Type, press the right arrow to select Atari Joystick
    • Press B to return to the previous menu
    • Select Resume and press A to return to the game
    • Test to see if it works now.  If it works, you can permanently save it by going back to Controls and selecting Save Core Overrides

Sinclair ZX Spectrum

Not all games will work with a joystick!  However, if the input does not seem to be working

  • Launch a ZX Spectrum game
  • Press the Select + X (Hotkey + X) to bring up the Quick Menu
    • Select Controls and press A
    • Next to user 1 Device Type, press the right arrow to select Kempton Joystick or Cursor Joystick
    • Press B to return to the previous menu
    • Select Resume and press A to return to the game
    • Test to see if it works now.  If it works, you can permanently save it by going back to Controls and selecting Save Core Overrides

Install Skyscraper

NOTE: See my article on cleaning ROM collections:
http://sigkillit.com/2019/06/19/clean-no-intro-2018-rom-set/

  • Navigate to the RetroPie menu and press OK on the keyboard
    • Select RETROPIE SETUP and press OK on the keyboard
    • Select Manage packages and press enter on the keyboard
    • Select Manage optional packages and press enter on the keyboard
    • Select Skyscraper and press Enter on the keyboard
    • Select Install from pre-compiled binary or Install from source and press Enter on the keyboard
      • Select Yes and press enter on the keyboard
    • Select Back and press Enter on the keyboard (Repeat 3x’s)
    • Select Exit and press Enter on the keyboard
  • Optional – Add Login for screenscraper.fr, do the follwoing:
    • Press f4 to exit emulationstation to the terminal and enter the command:
      • sudo nano /opt/retropie/configs/all/skyscraper/config.ini
    • Add the below lines
      • [screenscraper]
      • userCreds=”USER:PASSWORD”
      • Replace USER with your username and PASSWORD with your password
    • Press Ctrl + O then press Enter on the keyboard to save the file
    • Press Ctrl + X to exit
    • Type emulationstation and press Enter to go back to emulationstation

Scrape ROMS with Skyscraper

Scrape All Game Data

  • Press F4 on the keyboard to exit emulationstation and enter the terminal
    • Note: once you exit emulationstation, you can SSH into the PI so you can use your TV while you wait for the ROM scrapes to occur which take hours to days. The default username is pi and password is raspberry
  • Enter the following command at the terminal:
    • sudo ~/RetroPie-Setup/retropie_setup.sh
  • Select Manage Packages and press Enter on the keyboard
  • Select Optional Packages and press Enter on the keyboard
  • Select Skyscraper and press Enter on the keyboard
  • Select Configuration/Options and press Enter on the keyboard
  • Select Gather Source and press Enter on the keyboard
    • Choose the source, I recommend using ScreenScraper and TheGamesDB. SkyScraper will use both sources to give you the most complete data
  • Select Gather Resources and press Enter on the keyboard
    • Select the game system you want to scrape (suggest 1 at a time as it takes a few hours & the sources will limit the # of scrapes in a time limit) and press Enter on the keyboard
    • Note: This caches the game info & images in \\retropie\configs\all\skyscraper\ and only needs run once unless you add new games.  It then uses this cache to build the game info, which greatly increases the speed since it all local.  However, you will need to monitor the size of the cache to make sure it does not fill the SD card.
  • Select Generate Games Lists and press Enter on the keyboard
    • Select the game system(s) you want to scrape and press Enter on the keyboard
    • Note: This builds the game images, description, etc
    • Select Run on all systems and press Enter on the keyboard then WAIT!

Fix Incorrect or Missing Scrape Data From Screenscraper

I found what I consider to be errors or missing game data for some Games on the ScreenScraper DB or GameFAQs (Use it as a parent DB to add new Games) and submitted proposals to correct them or add data. However, some of my requests were denied. I’ve created custom fixes you can download here and the rest of the info will be pulled from TheGamesDB or Screenscraper as an additional source.

  • Download the zip file here
  • Unzip to file to \\retropie\configs\all\skyscraper\import\
  • You will need to Gather Resources and select Import as the Source

Here’s a complete list of the corrections I made:

  • coleco
    • Super Action Soccer (USA, Europe) – Named the game “Super Action Football (soccer)” which causes potential duplicates with “Super Action Football” since the root names match and the brackets should be reserved for info such as region. My suggestion was denied to rename the game to something without brackets such as “Super Action Football aka Soccer” or “Super Action Football ‘Soccer’”. My fix overrides the Title and forces it to use TheGamesDB as the data source for the rest of the game instead of Screenscraper.
  • gb
    • 4 in 1 (Europe) (4B-001, Sachen-Commin) (Unl)
    • 4 in 1 (Europe) (4B-002, Sachen) (Unl)
    • 4 in 1 (Europe) (4B-004, Sachen-Commin) (Unl)
    • 4 in 1 (Europe) (4B-005, Sachen-Commin) (Unl)
    • 4 in 1 (Europe) (4B-006, Sachen) (Unl)
    • 4 in 1 (Europe) (4B-007, Sachen) (Unl)
    • 4 in 1 (Europe) (4B-008, Sachen) (Unl)
    • 4 in 1 (Europe) (4B-009, Sachen) (Unl)
    • 4 in 1 (Taiwan) (En,Zh) (4B-003, Sachen-Commin) (Unl)
      • All of the above games scrape as “4-in-1 Fun Pak” instead of the proper “4 in 1” Sachen games. I tried submitting them as new games but was denied because Wikipedia was not a good enough source (https://en.wikipedia.org/wiki/Thin_Chen_Enterprise). My fix includes generic images and game info.
  • gba
    • 2 Games in 1 – Scooby-Doo + Scooby-Doo 2 – Monsters Unleashed (USA)
    • 2 in 1 – Asterix & Obelix – Bash Them All! + Asterix & Obelix XXL (Europe) (En,Fr,De,Es,It,Nl)
    • 2-in-1 Fun Pack – Shrek 2 + Madagascar (USA)
    • Donkey Kong (USA) (Advance Play Edition)
    • Double Game! – Golden Nugget Casino & Texas Hold ’em Poker (Europe)
    • Golf (USA) (Advance Play Edition)
    • Harry Potter Collection (Europe) (En,Fr,De,Es,It,Nl,Pt,Sv,No,Da)
    • Hugo 2 in 1 (Europe) (En,Fr,De,Es,It,Nl,Pt,Sv,No,Da,Fi,Pl)
    • Kaisertal – Fight the Necronis War (Europe) (Demo)
    • Pinball (USA) (Advance Play Edition)
    • Soccer (USA) (Advance Play Edition)
    • Tennis (USA) (Advance Play Edition)
    • Winnie the Pooh’s Rumbly Tumbly Adventure & Rayman 3 (Europe) (En,Fr,De,Es,It,Nl+En,Fr,De,Es,It,Nl,Sv,No,Da,Fi)
    • Yoshi’s Cookie (USA) (Advance Play Edition)
      • My requests to add descriptions on the above were denied because any source with info was not good enough. My fix includes a description for these games.
  • gbc
    • Jurassic Boy II + Thunder Blast Man (World) (1B-002, 1B-003, Sachen) (Unl)
    • Space Invasion & Karate Joe (Europe) (Unl)
    • Space Invasion & Painter (Europe) (Unl)
    • Space Invasion (Europe) (Unl)
      • My requests to add descriptions on the above were denied because any source with info was not good enough. My fix includes a description for these games.
  • snes
    • PowerFest 94 – Ken Griffey Jr. Presents Major League Baseball (USA)
    • PowerFest 94 – Super Mario Bros. – The Lost Levels (USA)
    • PowerFest 94 – Super Mario Kart (USA)
      • There is no separate game for the PowerFest 94 versions and it scrapes under the original game. My recommendation was denied because they consider it the same game with a timer. My fix overrides the Title and forces it to use TheGamesDB as the data source for the rest of the game instead of Screenscraper.
Jun 19

Clean No-Intro 2018 ROM Set

IMPORTANT UPDATE 11/05/2022 – This is updated for v5.6!!!

Overview

I did not want my RetroPie to be cluttered with ROMs I did not want. I only wanted the original games, I did not want duplicates or revisions, and I only wanted games in English so I could read the on screen text. For example, the NES has about 716 officially licensed games and the No-Intro 2018 ROM set has about 2,748 games in it. I wanted to reduce that number closer to the officially licensed games count to save space and reduce the amount of ROMs I would have to scroll through each time. I decided to create the below powershell scripts based on the No-Intro 2018 ROM set to accomplish my goal, which I’ll explain each step in detail.

http://sigkillit.com/wp-content/uploads/2022/11/Clean-NoIntro2018_v56.zip

You can also download the older archived scripts, but I HIGHLY RECOMMEND sticking with latest scripts!

The No-Intro 2018 ROM set includes the following game systems that are compatible with RetroPie/Emulationstation:

  • Atari: 2600
  • Atari: 5200
  • Atari: 7800
  • Atari: Jaguar
  • Atari: Lynx
  • Atari: ST
  • Bandai: WonderSwan
  • Bandai: WonderSwan Color
  • Coleco: ColecoVision
  • GCE: Vectrex
  • Microsoft: MSX
  • NEC: PC Engine / TurboGrafx 16
  • Nintendo: Family Computer Disk System
  • Nintendo: Game Boy
  • Nintendo: Game Boy Advance
  • Nintendo: Game Boy Color
  • Nintendo: Nintendo 64
  • Nintendo: Nintendo Entertainment System
  • Nintendo: Super Nintendo Entertainment System
  • Nintendo: Virtual Boy
  • SNK: Neo Geo Pocket
  • SNK: Neo Geo Pocket Color
  • Sega: 32X
  • Sega: Game Gear
  • Sega: Master System / Mark III
  • Sega: Mega Drive / Genesis
  • Sega: SG-1000

The No-Intro ROMs follow a pretty standardized naming scheme such as:

  • Battletoads (Europe).zip
  • Battletoads (Japan).zip
  • Battletoads (USA).zip
  • Captain Skyhawk (Europe).zip
  • Captain Skyhawk (USA) (Rev A).zip
  • Captain Skyhawk (USA).zip

As you can see, the base name of the ROM is followed by detailed descriptions such as the region released, revision, language, etc which is contained in parentheses. The script is based on using this naming scheme to get unique ROM base names by grabbing everything to the left of the first parentheses. [ Note: As of v5.5 you can use the $romSplitchar variable in each systems Filter to customize what character to split on, and by default I set it to a left Parenthesis ‘(‘ ]. It then further analyzes the results based on filters for each game system, which decide the ROM to keep. The default filters are set to prefer the USA version and the earliest revision (On older gaming systems, later revisions usually just made changes like the sprite used for a character or the color and not necessarily “bug” fixes like more modern game systems). Using the examples above, the script would keep “Battletoads (USA).zip) and “Captain Skyhawk (USA).zip”. See below for further details on using the scripts.

1_Clean-NoIntroRoms.ps1

See supplemental scripts:

Global Variable Definitions:

  • $global:scriptName
    • Name of the Script, defaults to full name of the script
  • $global:scriptVersion
    • Script Version
  • $global:romNoIntroDirectory
    • Source No-Intro 2018 Game System ROM Directory, each game system should have a folder in here that matches in $global:romNoIntroHashTable. Default is “C:\Games\No-Intro 2018\”
  • $global:romCleanDirectory
    • Destination directory to copy No-Intro 2018 ROMs to, each game system will have a folder in here that matches in $global:romNoIntroHashTable (Default is the RetroPie game system directory names). This way it never modifies the ROMs in the source directory. Default is “C:\Games\RetroPie\roms\”
  • $global:romCleanDirectoryPurge
    • Remove each game system directory in $global:romCleanDirectory so it does not exist before copying the ROMs. Default is $true
  • $global:romOverWrite
    • Overwrite existing ROMs in each game system directory in $global:romCleanDirectory if the game system directory exists. When $false, it will not copy any ROMs into the game system directory if it exists. When $true, it will copy and overwrite ROMs into the game system directory if it exists, but will not remove any additional ROMs or files that exist. Default is $false
  • $global:romFilterDirectory
    • Directory that stores the “filters” for each game system. The filters are .ps1 files named using the RetroPie directory name defined in $global:romNoIntroHashTable. I have customized the filters for each system and included them by default. You can modify them to your liking or if they do not exist, the script will generate a generic filter for each system. Default is a “filters” folder in the same directory as the script
  • $global:romRetropieDirectory
    • Location of the ROM directory on the RetroPie, which is used if you want the script to auto copy the ROMs to your RetroPie. Default is “\\retropie\roms\”
  • $global:romRetropieDirectoryPurge
    • Purge each game system’s ROM directory before copying ROMs to the RetroPie. Default is $true
  • $global:romRetropieUpload
    • Auto copy the ROMs from $global:romCleanDirectory to $global:romRetropieDirectory. Default is $true
  • $global:logFile
    • Log file output of the entire run and additional log files are placed in the same directory. Default is a “logs” directory in the same directory as the script with a log name that matches the script’s base name. Ex: .\logs\1_Clean-NoIntroRom.log
  • $global:logOverwrite
    • Overwrite the log files. Default is $true
  • $global:logVerbose
    • Show the full details of how it decides which ROMs to keep. This should only be used for debugging purposes and will increase the run time of the script. Default is $false
  • $global:romNoIntroHashTable
    • Hashtable setting each game systems directory name on RetroPie and in the No-Intro 2018 complete ROM set. Default directory names are used for each

Filter File Variable Definitions:

I have customized and included these since v5.0 and they are located in .\filters\<gamesystem>.ps1. If you want customize which ROMs are kept for each system, this is what you want to change.

  • $romSplitchar
    • Customize the character used to split a ROM name to determine unique ROM names by using everything to the left of this character. By default this is set to a left Parenthesis ‘(‘. For example, when comparing “Darkwing Duck (USA).zip” and “Darkwing Duck (Europe).zip”, the unique ROM name would be “Darkwing Duck “. The script would then use $romsSortOrder to determine which of the two files to keep, which would be the USA version if you use my default filters.
  • $romUnzip
    • Unzip ROMs for emulators that don’t support .zip. $false will NOT unzip them and $true will unzip them
  • $romsDoNotDelete
    • Any exact ROM name (including extension) in this list will NOT be deleted. ROMs listed here will not be deleted even in they match a value in $romsExceptions or they are NOT the Primary ROM via $romSortOrder
    • Ex: “Castlevania (USA) (Rev A).zip”
  • $romsExceptions
    • Any ROMs containing these strings or exact ROM name will be removed unless the exact ROM name is in $romsDoNotDelete
  • $romsSortOrder
    • Preferred sorting order to pick 1 Primary ROM to keep when evaluating duplicates. Uses regular expressions to define the preferred sorting order based on language, and if there are no matches it will determine the primary ROM by alphabetical sort. ROMs in $romsExceptions will not be deleted even if it is not determined to be the Primary ROM
#1_Clean-NoIntroRoms.ps1
#v5.6
# -Added Description to Filters
# -Added 4_UpdateVersion.ps1 Script
#v5.5
# -Added $romSplitchar variable to Filters to customize how unique ROM names are determined
#  -Everything to the left of this character is determined to be the unique ROM name.  
#  -If left blank, all ROMs will be skipped. Recommended to keep default '(' for No-Intro ROMs
# -Fixed logs in 1_Clean-NoIntroRoms_Exceptions.log so they stay in order
#v5.4
# -Fixed bug in Get-NoIntroRomsExceptions When ROM Name Does Not Contain '('
# -Fixed bug setting non-duplicate primary ROM due to name missing $romSplitchar resulting in 0 matches
# -Updated Filters by adding ROM extensions as last resort when matching, otherwise primary ROM is never set
# -Added $global:scriptVersion to Set Filter Version in Get-NoIntroDefaultFilters Function
#v5.3
# -Re-Added Function Get-NoIntroDefaultFilters which was accidently removed from version 5.2
# -Renamed "config" to "filter" Main Function Section 1 Log
# -Added Changelog in Comments
#v5.2
# -Added variables for Write-Out Colors and Updated to Write-Out v1.3
# -Updated Filters for scraping
#v5.1
# -Fixed minor bugs and updating filters for scraping
#v5.0
# -Rewrote the code to be modular by importing filters for each game system instead of using separate scripts
# -Added variables to define where to store game system filters and logs
# -Replaced $MyInvocation.MyCommand.Name with $PSCommandPath which requires Powershell 3 or greater
# -Replaced Log-Write with Write-Out function which requires Powershell 4 or greater
# -Added Atari Jaguar because it can run on PI4 with proper customization
# -Fixed bug with romsDoNotDelete
# -Fixed bug where exceptions were not removed from uniqueRom causing it to still evaluate them for duplicates
$global:scriptName = $PSCommandPath
$global:scriptVersion = "v5.6"
$global:romNoIntroDirectory = "C:\Games\Complete ROM Sets\No-Intro 2018\"
$global:romCleanDirectory = "C:\Games\RetroPie\roms\"
$global:romCleanDirectoryPurge = $true
$global:romOverWrite = $false
$global:romFilterDirectory = (Split-Path -Path $PSCommandPath -Parent) + "\filters\"
$global:romRetropieDirectory = "\\retropie\roms\"
$global:romRetropieDirectoryPurge = $true
$global:romRetropieUpload = $true
$global:logFile = (Split-Path -Path $PSCommandPath -Parent) + "\logs\" + (Split-Path -Path $PSCommandPath.Replace(".ps1",".log") -Leaf)
$global:logOverwrite = $true
$global:logVerbose = $false
$global:romNoIntroHashTable = [ordered]@{
	#RetroPieDirName = No-IntroDirName
	"atari2600" = "Atari - 2600" 
	"atari5200"  = "Atari - 5200"
	"atari7800" = "Atari - 7800 [Headered]"
	"atarijaguar" = "Atari - Jaguar"
	"atarilynx" = "Atari - Lynx [Headered]"
	"atarist" = "Atari - ST"
	"coleco" = "Coleco - ColecoVision"
	"fds" = "Nintendo - Family Computer Disk System [headered]"
	"gamegear" = "Sega - Game Gear"
	"gb" = "Nintendo - Game Boy"
	"gba" = "Nintendo - Game Boy Advance"
	"gbc" = "Nintendo - Game Boy Color"
	"mastersystem" = "Sega - Master System - Mark III"
	"megadrive" = "Sega - Mega Drive - Genesis"
	"msx" = "Microsoft - MSX"
	"n64" = "Nintendo - Nintendo 64"
	"nes" = "Nintendo - Nintendo Entertainment System [headered]"
	"ngp" = "SNK - Neo Geo Pocket"
	"ngpc" = "SNK - Neo Geo Pocket Color"
	"pcengine" = "NEC - PC Engine - TurboGrafx 16"
	"sega32x" = "Sega - 32X"
	"sg-1000" = "Sega - SG-1000"
	"snes" = "Nintendo - Super Nintendo Entertainment System"
	"vectrex" = "GCE - Vectrex"
	"virtualboy" = "Nintendo - Virtual Boy"
	"wonderswan" = "Bandai - WonderSwan"
	"wonderswancolor" = "Bandai - WonderSwan Color"
}
#Output Color Variables
$global:colorMain = "Green"
$global:colorSub1 = "DarkGreen"
$global:colorSub2 = "Green"
$global:colorSub3 = "DarkRed"
$global:colorOk = "Green"
$global:colorWarn = "Yellow"
$global:colorError = "Red"



Function Main()
{	
	#Powershell 4 or greater is required
	If (!($PSVersionTable.PSVersion.Major -ge 4) -OR ($PSVersionTable.PSVersion -eq $null))
	{
		Write-Host -ForegroundColor $global:colorError "Powershell v4 or greater is required, quitting script!"
		Exit
	}
	
	#Create Log File
	If ($global:logOverwrite -eq $true)
	{
		Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile -Overwrite
	}
	Else 
	{
		Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	}
	Write-Out -ForegroundColor $global:colorMain -Text "SECTION 1: INITIALIZE" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Script Name             : $($global:scriptName)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Version:                : $($global:scriptVersion)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM No-Intro Dir        : $($global:romNoIntroDirectory)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Clean Dir           : $($global:romCleanDirectory)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Clean Dir Purge     : $($global:romCleanDirectoryPurge)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Overwrite           : $($global:romOverWrite)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Filter Dir          : $($global:romFilterDirectory)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Retropie Dir        : $($global:romRetropieDirectory)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Retropie Dir Purge  : $($global:romRetropieDirectoryPurge)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Upload to Retropie  : $($global:romRetropieUpload)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Log File                : $($global:logFile)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Log Overwrite           : $($global:logOverwrite)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Log Verbose             : $($global:logVerbose)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Description             : Cleanup 2018 No-Intro ROMs" -LogFile $global:logFile
	$tStart = (Get-Date)
	Write-Out -ForegroundColor $global:colorMain -Text "* Start Time              : $($tStart)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile

	$tLog = $global:logFile.Replace(".log","_Exceptions.log")
	$tFilterDir = $global:romFilterDirectory 
	Write-Out -PreLine 1 -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "SECTION 2: BUILD LIST OF UNIQUE EXCEPTIONS FILTERS FOR NO-INTRO ROMS" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Log File                : $($tLog)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Log Overwrite           : $($global:logOverwrite)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Description             : Use No-Intro Naming Scheme to Build a List of Unique Exception Filters Used to Remove" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "*                           Unwanted ROMs For Each System Ex: (Beta), (Proto),(Japan)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	$ArgumentHash = @{
		Source = $global:romNoIntroDirectory
		FilterDir = $tFilterDir
		HashTable = $global:romNoIntroHashTable
		Log = $tLog 
		LogOverwrite = $global:logOverwrite
	}
	Get-NoIntroRomsExceptions @ArgumentHash

	Write-Out -PreLine 1 -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "SECTION 3: CLEANUP NO-INTRO ROMS" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Log File                : Per Game System" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Log Overwrite           : $($global:logOverwrite)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Description             : Use filters to copy No-Intro 2018 ROMs from Source to clean directory" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	ForEach ($key in $global:romNoIntroHashTable.keys)
	{
		$tLog = "$((Split-Path -Path $global:logFile -Parent))\$($key).log"
		$tFilters = "$($global:romFilterDirectory)$($key).ps1"
		$tSource = "$($global:romNoIntroDirectory)$($global:romNoIntroHashTable[$key])"
		$tDestination = "$($global:romCleanDirectory)$($key)"
		Write-Out -PreLine 1 -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorMain -Text "PROCESS: $key" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Description         : $($global:romNoIntroHashTable[$key])" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Filters             : $($tFilters)" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Source Dir          : $($tSource)" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Clean Dir           : $($tDestination)" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Clean Dir Purge     : $($global:romCleanDirectoryPurge)" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Overwrite           : $($global:romOverWrite)" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "Log File                : $($tLog)" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "Log Overwrite           : $($global:logOverwrite)" -LogFile $global:logFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "Log Verbose             : $($global:logVerbose)" -LogFile $global:logFile
		If ($global:romCleanDirectoryPurge)
		{
			If(Test-Path -LiteralPath "$($tDestination)")
			{
				Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Clean Dir Exists, Purge Before Processing" -LogFile $global:logFile
				Write-Out -ForegroundColor DarkRed -Text "Purge: $($tDestination)" -LogFile $global:logFile
				Remove-Item "$($tDestination)" -Recurse -Force -Confirm:$false
			}
			Else
			{
				Write-Out -ForegroundColor $global:colorWarn -Text "ROM Clean Dir Does Not Exist, Skip Purge Before Processing" -LogFile $global:logFile
			}
		}
		$ArgumentHash = @{
			System = $key
			Description = $global:romNoIntroHashTable[$key] 
			Source = $tSource 
			Destination = $tDestination 
			Overwrite = $global:romOverWrite
			Filters = $tFilters
			Log = $tLog 
			LogOverwrite = $global:logOverwrite 
			LogVerbose = $global:logVerbose
		}
		Clean-NoIntroRoms @ArgumentHash
		Write-Out -ForegroundColor $global:colorSub1 -Text "Processing $($key) Complete" -LogFile $global:logFile
	}	
	
	Write-Out -PreLine 1 -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "SECTION 4: END RESULTS" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Retropie Dir        : $($global:romRetropieDirectory)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Retropie Dir Purge  : $($global:romRetropieDirectoryPurge)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* ROM Upload to Retropie  : $($global:romRetropieUpload)" -LogFile $global:logFile
	If ($global:romRetropieUpload)
	{
		#Loop Each ROM Directory and Copy to RetroPie
		ForEach ($key in $global:romNoIntroHashTable.keys)
		{
			$tSource = "$($global:romCleanDirectory)$($key)"
			$tDestination = "$($global:romRetropieDirectory)$($key)"
			Write-Out -PreLine 1 -ForegroundColor $global:colorMain -Text "* $($key)" -LogFile $global:logFile
			Write-Out -ForegroundColor $global:colorMain -Text "* =======================================================" -LogFile $global:logFile
			If (Test-Path -LiteralPath "$($tSource)")
			{
				Write-Out -ForegroundColor $global:colorMain -Text "* Source Exist: $($tSource)" -LogFile $global:logFile
				If (Test-Path -LiteralPath "$($tDestination)")
				{
					Write-Out -ForegroundColor $global:colorMain -Text "* Dest Exist: $($tDestination)" -LogFile $global:logFile
					If ($global:romRetropieDirectoryPurge)
					{
						Write-Out -ForegroundColor $global:colorError -Text "* Purge: $($tDestination)" -LogFile $global:logFile
						Get-ChildItem "$($tDestination)" -Recurse | Remove-Item -Force -Confirm:$false
					}
					Write-Out -ForegroundColor $global:colorMain -Text "* Copy ROMs from '$($tSource)' to '$($tDestination)'" -LogFile $global:logFile
					Copy-Item -Path "$($tSource)\*" -Destination "$($tDestination)" -Recurse -Force
				}
				Else 
				{
					Write-Out -ForegroundColor $global:colorWarn -Text "* Dest NOT Exist: $($tDestination)" -LogFile $global:logFile
					Write-Out -ForegroundColor $global:colorMain -Text "* Create Folder: $($tDestination)" -LogFile $global:logFile
					If (New-Item -ItemType Directory -Path $tDestination -Force)
					{
						Write-Out -ForegroundColor $global:colorMain -Text "* Copy ROMs from '$($tSource)' to '$($tDestination)'" -LogFile $global:logFile
						Copy-Item -Path "$($tSource)\*" -Destination "$($tDestination)" -Recurse -Force
					}
					Else
					{
						Write-Out -ForegroundColor $global:colorError -Text "* ERROR Creating Destination No ROMs Copied!" -LogFile $global:logFile
					}
				}
			}
			Else
			{
				Write-Out -ForegroundColor $global:colorError -Text "* Source NOT Exist: $($tSource)" -LogFile $global:logFile
				Write-Out -ForegroundColor $global:colorError -Text "* ERROR CANNOT COPY!!!" -LogFile $global:logFile
			}
		}
	}
	$tEnd = (Get-Date)
	Write-Out -ForegroundColor $global:colorMain -Text "* End Time                : $($tEnd)" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "* Elapsed Time            : $([math]::Round(($tEnd-$tStart).totalseconds, 2)) seconds" -LogFile $global:logFile
	Write-Out -ForegroundColor $global:colorMain -Text "****************************************************************" -LogFile $global:logFile
}



Function Get-NoIntroRomsExceptions()
{
	Param
	(	
		[Parameter(Mandatory=$true)]
		[Alias("Source")] 
		[string]$RomDir,
		
		[Parameter(Mandatory=$true)]
		[string]$FilterDir = "",
		
		[Parameter(Mandatory=$true)]
		[Alias("HashTable")] 
		[System.Collections.Specialized.OrderedDictionary]$RomHashTable,
		
		[Parameter(Mandatory=$true)]
		[Alias("Log")]
		[string]$LogFile,
		
		[Parameter(Mandatory=$false)]
		[string]$LogOverwrite = $false
	)
	If ($LogOverwrite -eq $true)
	{
		Write-Out -ForegroundColor $global:colorMain -Text " " -LogFile $LogFile -Overwrite
	}
	Else 
	{
		Write-Out -ForegroundColor $global:colorMain -Text " " -LogFile $LogFile
	}
	
	$colUniqueRomsAll = @()
	ForEach ($key in $RomHashTable.keys)
	{
		#Process Current ROM Directory
		Write-Out -PreLine 1 -ForegroundColor $global:colorMain -Text "PROCESS: $($RomDir)$($RomHashTable[$key])" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorMain -Text "=======================================================" -LogFile $LogFile
		
		#Confirm No-Intro ROM Directory Exists
		If(!(Test-Path -LiteralPath "$($RomDir)$($RomHashTable[$key])"))
		{
			#Directory Does Not Exist
			Write-Out -ForegroundColor $global:colorError -Text "ERROR: ROM Directory Does Not Exist!!!" -LogFile $LogFile
		}
		Else 
		{
			#Directory Exists
			Write-Out -ForegroundColor $global:colorSub1 -Text "Directory Exists: Continue Processing" -LogFile $LogFile
			
			#Check If Filter Exists Then Set $romSplitChar
			If (Test-Path -LiteralPath "$($FilterDir)$($key).ps1")
			{
				Write-Out -ForegroundColor $global:colorSub1 -Text "Filter Exists: Import Unique Split Char Variable" -LogFile $LogFile
				. "$($FilterDir)$($key).ps1"
				If ([string]::IsNullOrEmpty($romSplitchar))
				{
					Write-Out -ForegroundColor $global:colorSub1,$global:colorWarn -Text "Unique Split Char: ","Null Or Empty Do Not Process" -LogFile $LogFile
				}
				Else
				{
					Write-Out -ForegroundColor $global:colorSub1 -Text "Unique Split Char: '$($romSplitchar)'" -LogFile $LogFile
					
					
					$colRoms = @()
					$colUniqueRoms = @()
					$colRoms = Get-ChildItem -LiteralPath "$($RomDir)$($RomHashTable[$key])"
					$colRoms | ForEach-Object `
					{
						If ($_.BaseName.Contains($romSplitchar))
						{
							$colUniqueRoms += $romSplitchar + $_.BaseName.split($romSplitchar,2)[1]
						}
					}
					$colUniqueRoms = $colUniqueRoms | Sort-Object -Unique
					$colUniqueRomsAll += $colUniqueRoms
					Write-Out -ForegroundColor $global:colorMain -Text "Found $($colUniqueRoms.Count) Potential Exceptions to Filter:" -LogFile $LogFile
					ForEach ($name in $colUniqueRoms)
					{
						Write-Out -ForegroundColor $global:colorSub1 -Text "$($name)" -LogFile $LogFile
					}
				}
			}
			Else
			{
				Write-Out -ForegroundColor $global:colorWarn -Text "Filter Not Exist: Do Not Process" -LogFile $LogFile
			}
		}
	}
	Write-Out -PreLine 1 -ForegroundColor $global:colorMain -Text "PROCESS: Combined Unique Exceptions" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorMain -Text "=======================================================" -LogFile $LogFile
	$colUniqueRomsAll = $colUniqueRomsAll | Sort-Object -Unique
	Write-Out -ForegroundColor $global:colorMain -Text "Found $($colUniqueRomsAll.Count) Combined Potential Exceptions to Filter:" -LogFile $LogFile
	ForEach ($name in $colUniqueRomsAll)
	{
		Write-Out -ForegroundColor $global:colorSub1 -Text "$($name)" -LogFile $LogFile
	}
}



Function Clean-NoIntroRoms()
{
	Param
	(
			
		[Parameter(Mandatory=$true)]
		[Alias("System")]
		[string]$GameSystem,
		
		[Parameter(Mandatory=$true)]
		[Alias("Description")]
		[string]$GameDescription,
	
		[Parameter(Mandatory=$true)]
		[Alias("Source")]
		[string]$RomSource,
		
		[Parameter(Mandatory=$true)]
		[Alias("Destination")]
		[string]$RomDestination,
		
		[Parameter(Mandatory=$true)]
		[Alias("Overwrite")]
		[bool]$RomOverwrite,
		
		[Parameter(Mandatory=$true)]
		[Alias("Filters")]
		[string]$RomFilters,
		
		[Parameter(Mandatory=$true)]
		[Alias("Log")]
		[string]$LogFile,
		
		[Parameter(Mandatory=$false)]
		[bool]$LogOverwrite = $true,
		
		[Parameter(Mandatory=$false)]
		[bool]$LogVerbose = $false		
	)
	$countTotal  = 0
	$countUnique = 0
	$countExceptionsDeleted = 0
	$countDuplicatesDeleted = 0
	$countNonDuplicatesDeleted = 0

	#Create Log File
	If ($LogOverwrite -eq $true)
	{
		Write-Out -ForegroundColor $global:colorMain -Text "$($GameSystem.ToUpper()) (STEP 1/6): Processing Information" -LogFile $LogFile -Overwrite
	}
	Else 
	{
		Write-Out -ForegroundColor $global:colorMain -Text "$($GameSystem.ToUpper()) (STEP 1/6): Processing Information" -LogFile $LogFile
	}
	Write-Out -ForegroundColor $global:colorMain -Text "================================================================" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "Game System             : $($GameDescription)" -LogFile $LogFile
	$timeStart = (Get-Date)
	Write-Out -ForegroundColor $global:colorSub1 -Text "Start Time              : $($timeStart)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Source Dir          : $($RomSource)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Clean  Dir          : $($RomDestination)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROMs Overwrite          : $($RomOverwrite)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROM Filters             : $($RomFilters)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "Log File                : $($LogFile)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "Log Overwrite           : $($LogOverwrite)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "Log Verbose             : $($LogVerbose)" -LogFile $LogFile
	If(!(Test-Path -LiteralPath $RomSource))
	{
		Write-Out -ForegroundColor $global:colorError -Text "ERROR: ROM Source Directory Does Not Exist - Stop Processing!" -LogFile $LogFile
		Return
	}
	If(!(Test-Path -LiteralPath $RomFilters))
	{
		#Set Defaults For: $romUnzip, $romsDoNotDelete, $romsExceptions, $romsSortOrder
		Write-Out -ForegroundColor $global:colorWarn -Text "WARNING: ROM Filters File Does Not Exist, Set Default Values - Continue Processing!" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorWarn -Text "$($RomFilters): Write `$romSplitchar Default" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorWarn -Text "$($RomFilters): Write `$romUnzip Default" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorWarn -Text "$($RomFilters): Write `$romsDoNotDelete Default" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorWarn -Text "$($RomFilters): Write `$romsExceptions Default" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorWarn -Text "$($RomFilters): Write `$romsSortOrder Default" -LogFile $LogFile
		$DefaultFilters = Get-NoIntroDefaultFilters	(Split-Path -Path $RomFilters -Leaf)
		Write-Out -ForegroundColor $global:colorWarn -Text "Write to File: $($RomFilters)" -LogFile $LogFile
		$nf = New-Item -Path $RomFilters -type file -force -Value $DefaultFilters
	}

	#Set Custom Variables to Cleanup ROMs for Each System by Dot Sourcing($romUnzip, $romsDoNotDelete, $romsExceptions, $romsSortOrder)
	Write-Out -ForegroundColor $global:colorSub1 -Text "Import Variables from $($RomFilters)" -LogFile $LogFile
	. "$($RomFilters)"
	Write-Out -ForegroundColor $global:colorMain -Text "ROM Exceptions to Keep" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorMain -Text "-------------------------------------------------------" -LogFile $LogFile
	If (($romsDoNotDelete.Count -le 0) -OR $romsDoNotDelete -eq $null)
	{
			Write-Out -ForegroundColor $global:colorWarn -Text "None" -LogFile $LogFile
	}
	Else 
	{
		ForEach ($rom in $romsDoNotDelete)
		{
			Write-Out -ForegroundColor $global:colorSub1 -Text "$($rom)" -LogFile $LogFile
		}
	}
	
	Write-Out -ForegroundColor $global:colorMain -Text "Delete ROMs Containing" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorMain -Text "-------------------------------------------------------" -LogFile $LogFile
	If (($romsExceptions.Count -le 0) -OR $romsExceptions -eq $null)
	{
			Write-Out -ForegroundColor $global:colorWarn -Text "None" -LogFile $LogFile
	}
	Else 
	{
		ForEach ($rom in $romsExceptions)
		{
			Write-Out -ForegroundColor $global:colorSub1 -Text "$($rom)" -LogFile $LogFile
		}
	}
	
	Write-Out -ForegroundColor $global:colorMain -Text "ROM Sort Order" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorMain -Text "-------------------------------------------------------" -LogFile $LogFile
	ForEach ($rom in $romsSortOrder)
	{
		Write-Out -ForegroundColor $global:colorSub1 -Text "$($rom)" -LogFile $LogFile
	}
	

	
	#Get List of ROMs and Unique ROMs in the original No-Intro 2018 ROM set
	Write-Out -ForegroundColor $global:colorMain -Text "$($GameSystem.ToUpper()) (STEP 2/6): Get All No-Intro ROMs and Unique ROMs to Process" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorMain -Text "================================================================" -LogFile $LogFile	
	[System.Collections.ArrayList]$colRoms = @()
	(Get-ChildItem -LiteralPath $RomSource).ForEach({$obj = [PSCustomObject]@{ FullName = $_.FullName; Name = $_.Name};$colRoms.add($obj)|out-null})
	If ([string]::IsNullOrEmpty($romSplitchar))
	{
		#No Split Character to Determine Unique ROM Name
		Write-Out -ForegroundColor $global:colorWarn -Text "No Split Character to Determine Unique ROM Name" -LogFile $LogFile
		
		#Remove All ROMs
		$colRoms.Clear()
	}
	Else
	{
		$colUniqueRoms = @()
		$colUniqueRoms = $colRoms | ForEach-Object { $_.Name.split($romSplitchar)[0] } | Sort-Object -Unique
		
		#Count Total Original Source ROMs and Original Source Unique ROMs
		$countTotal = $colRoms.count
		$countUnique = $colUniqueRoms.count
		Write-Out -ForegroundColor $global:colorSub1 -Text "Total ROMs: $($countTotal)" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorSub1 -Text "Total Unique ROMs: $($countUnique)" -LogFile $LogFile



		#Remove Exceptions - Further Speeds Things Up By Not Having to Process As Much
		Write-Out -ForegroundColor $global:colorMain -Text "$($GameSystem.ToUpper()) (STEP 3/6): Remove Exceptions" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorMain -Text "================================================================" -LogFile $LogFile
		If (($romsExceptions.Count -le 0) -OR $romsExceptions -eq $null)
		{
				Write-Out -ForegroundColor $global:colorWarn -Text "None" -LogFile $LogFile
		}
		Else 
		{
			#Remove Exception from colRoms Array
			For ($i=0; $i -lt $colRoms.Count; $i++)
			{
				ForEach ($romException in $romsExceptions) 
				{
					#Check If Current ROM Matches Exception
					If ($colRoms[$i].Name -match [regex]::escape($romException))
					{
						#Exception Found (Flag Delete)
						#Special Case Do Not Delete ROMs Check
						$romDND = $false
						If ($romsDoNotDelete.Count -gt 0)
						{
							ForEach ($doNotDelete in $romsDoNotDelete)
							{
								If ($doNotDelete -eq $colRoms[$i].Name)
								{
									#Found Special Case (Flag NOT Delete)
									$romDND = $true
									break
								}
							}
						}
						#Check to Delete or Not
						If ($romDND -eq $true)
						{
							Write-Out -ForegroundColor $global:colorSub1 -Text "DO NOT DELETE EXCEPTON (Special Case): $($colRoms[$i].Name)" -LogFile $LogFile
						}
						Else
						{
							#Delete ROM
							Write-Out -ForegroundColor DarkRed -Text "DELETE EXCEPTION '$($romException)': $($colRoms[$i].Name)" -LogFile $LogFile
							$colRoms.RemoveAt($i)
							$i--
							$countExceptionsDeleted++
						}
						break
					}
				}
			}
		}
		#Update Unique ROMs to Remove Exceptions
		$colUniqueRoms = $colRoms | ForEach-Object { $_.Name.split($romSplitchar)[0] } | Sort-Object -Unique

		
		
		#Identify Primary ROM to Keep and Remove Duplicates
		Write-Out -ForegroundColor $global:colorMain -Text "$($GameSystem.ToUpper()) (STEP 4/6): Identify Primary ROM and Remove Duplicates" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorMain -Text "================================================================" -LogFile $LogFile
		ForEach ($uniqueRom in $colUniqueRoms)
		{
			#Determine Duplicates For Each Unique ROM Name
			$colDuplicateRoms = @()
			#$colDuplicateRoms = $colRoms.Name -match ("^"+[regex]::escape($uniqueRom)+"\(")
			$colDuplicateRoms = $colRoms.Name -match ("^"+[regex]::escape($uniqueRom)+[regex]::escape($romSplitchar))
			Write-Out -ForegroundColor $global:colorMain -Text "PROCESS: '$($uniqueRom)'" -LogFile $LogFile
			If ($colDuplicateRoms.count -gt 1)
			{
				#Determine Primary ROM In Each Duplicate ROM Set By Matching Sorting Preference
				:regloop ForEach ($sortRegex in $romsSortOrder )
				{
					#Process ROMS That Match A Sorting Order
					$primaryRom = ""
					$colRomsToSort = @()
					[array]$colRomsToSort = $colDuplicateRoms -match $sortRegex
					$colRomsToSort = $colRomsToSort | Sort-Object
					If ($colRomsToSort.count -ge 1)
					{
						#Find Primary ROM To Keep
						If ($LogVerbose -eq $true)
						{
							Write-Out -ForegroundColor $global:colorSub1 -Text "TEST REGEX ($($colRomsToSort.count) Matches): $($sortRegex)" -LogFile $LogFile
						}
						For($i = $colRomsToSort.GetLowerBound(0); $i -le $colRomsToSort.GetUpperBound(0); $i++) 
						{
							$matchRomException = $false
							If ($LogVerbose -eq $true)
							{
								Write-Out -ForegroundColor $global:colorSub1 -Text "Primary ROM Found (Match $($i + 1) of $($colRomsToSort.GetUpperBound(0) + 1)): $($colRomsToSort[$i])" -LogFile $LogFile
							}
							#Check Primary ROM For Exceptions
							ForEach ($romException in $romsExceptions) 
							{
								If ($colRomsToSort[$i] -match [regex]::escape($romException))
								{
									$matchRomException = $true
									If ($LogVerbose -eq $true)
									{
										Write-Out -ForegroundColor $global:colorSub1 -Text "Primary ROM Exception Found (Match $($i+1) of $($colRomsToSort.count): $($romException)" -LogFile $LogFile
									}
									break
								}
							}
							#Set Primary ROM 
							If ($matchRomException -eq $false)
							{
								$primaryRom = $colRomsToSort[$i]
								break regloop
							}
						}
					}
					Else 
					{
						If ($LogVerbose -eq $true)
						{
							Write-Out -ForegroundColor $global:colorSub1 -Text "SKIP REGEX (No Matches): $($sortRegex)" -LogFile $LogFile
						}
					}
				}
				
				#Display Primary ROM Result
				If ($primaryRom -eq "")
				{
					If ($LogVerbose -eq $true)
					{
						Write-Out -ForegroundColor $global:colorSub1 -Text "Primary ROM Set: No Primary Found!" -LogFile $LogFile
					}
				}
				Else
				{
					If ($LogVerbose -eq $true)
					{
						Write-Out -ForegroundColor $global:colorSub1 -Text "Primary ROM Set: $($primaryRom)" -LogFile $LogFile
					}
				}
				
				#Remove All Duplicate ROMS Except Primary ROM
				ForEach ($duplicateRom in $colDuplicateRoms)
				{
					#Primary ROM Found
					If ($duplicateRom -eq $primaryRom)
					{
						#Keep Primary ROM
						Write-Out -ForegroundColor $global:colorSub1 -Text "KEEP(Primary): $($duplicateRom)" -LogFile $LogFile
					}
					Else
					{
						#Special Case Do Not Delete ROMs Check
						$romDND = $false
						If ($romsDoNotDelete.Count -gt 0)
						{
							ForEach ($doNotDelete in $romsDoNotDelete)
							{
								If ($doNotDelete -eq $duplicateRom)
								{
									$romDND = $true
									break
								}
							}
						}
						
						If ($romDND -eq $true)
						{
							Write-Out -ForegroundColor $global:colorSub1 -Text "DO NOT DELETE (Special Case): $($duplicateRom)" -LogFile $LogFile
						}
						Else 
						{
							If ($primaryRom -eq "")
							{
								#No Primary Found Delete Duplicate
								Write-Out -ForegroundColor DarkRed -Text "DELETE(No Primary Found): $($duplicateRom)" -LogFile $LogFile
							}
							Else
							{
								#Not The Primary ROM Delete Duplicate Or Exception
								Write-Out -ForegroundColor DarkRed -Text "DELETE(Duplicate/Exception): $($duplicateRom)" -LogFile $LogFile
							}					
							#Delete ROM
							For ($i=0; $i -lt $colRoms.Count; $i++)
							{
								If ($colRoms[$i].Name -eq $duplicateRom)
								{
									$colRoms.RemoveAt($i)
									$i--
								}
							}
							$countDuplicatesDeleted++
						}
					}			
				}
			}
			Else 
			{
				#NOTE - $colDuplicateRoms acts as a variable (type=object) because there's a single item in the array!
				#If No Matches Found Manualy Set $colDuplicateRoms (Means ROM did not have $romSplitchar and returned file name + extension)
				If ($colDuplicateRoms.count -eq 0)
				{
					$colDuplicateRoms = $uniqueRom
				}
				
				#Non-Duplicate ROM Primary By Default
				If ($LogVerbose -eq $true)
				{
					Write-Out -ForegroundColor $global:colorSub1 -Text "SKIP REGEX (No Duplicates):" -LogFile $LogFile
					Write-Out -ForegroundColor $global:colorSub1 -Text "Primary ROM Set (Non-Duplicate): $($colDuplicateRoms)" -LogFile $LogFile
				}
				#Check Primary ROM For Exceptions
				$matchRomException = $false
				ForEach ($romException in $romsExceptions) 
				{
					#Check If Current ROM Matches Exception
					If ($colDuplicateRoms -match [regex]::escape($romException))
					{
						#Exception Found (Flag Delete)
						$matchRomException = $true
						#Special Case Do Not Delete ROMs Check
						$romDND = $false
						If ($romsDoNotDelete.Count -gt 0)
						{
							ForEach ($doNotDelete in $romsDoNotDelete)
							{
								If ($doNotDelete -eq $colDuplicateRoms)
								{
									#Found Special Case (Flag NOT Delete)
									$romDND = $true
									break
								}
							}
						}
						break
					}
				}
				#Check to Delete or Not 
				If ($matchRomException -eq $false)
				{
					Write-Out -ForegroundColor $global:colorSub1 -Text "KEEP(Primary Non-Duplicate): $($colDuplicateRoms)" -LogFile $LogFile
				}
				Else 
				{
					#Delete
					If ($romDND -eq $true)
					{
						#Special Case Keep
						Write-Out -ForegroundColor $global:colorSub1 -Text "DO NOT DELETE (Special Case Non-Duplicate): $($colDuplicateRoms)" -LogFile $LogFile
					}
					Else 
					{
						#Delete ROM 
						Write-Out -ForegroundColor DarkRed -Text "DELETE (Exception Non-Duplicate) '$($romException)': $($colDuplicateRoms)" -LogFile $LogFile
						For ($i=0; $i -lt $colRoms.Count; $i++)
						{
							If ($colRoms[$i].Name -eq $duplicateRom)
							{
								$colRoms.RemoveAt($i)
								$i--
							}
						}
					}
				}
			}
		}
	}
	
	#Copy NoIntro ROMs
	Write-Out -ForegroundColor $global:colorMain -Text "$($GameSystem.ToUpper()) (STEP 5/6): Copy No-Intro ROMs" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorMain -Text "================================================================" -LogFile $LogFile
	#Confirm Source ROM Directory Exists
	If (Test-Path -LiteralPath $RomSource)
	{
		Write-Out -ForegroundColor $global:colorSub1 -Text "Source Exist: '$($RomSource)'" -LogFile $LogFile
		
		#Confirm Destination Directory Exists
		If (Test-Path -LiteralPath $RomDestination)
		{
			#Destination Directory Exists - Overwrite?
			If ($RomOverwrite -eq $true)
			{
				Write-Out -ForegroundColor $global:colorWarn -Text "Destination EXIST (Overwrite): '$($RomDestination)'" -LogFile $LogFile
				Write-Out -ForegroundColor $global:colorSub1 -Text "Copy $($colRoms.count) ROMs from '$($RomSource)*' to '$($RomDestination)'" -LogFile $LogFile
				ForEach ($rom in $colRoms)
				{
					Copy-Item -LiteralPath $rom.FullName -Destination $RomDestination -Recurse -Force
				}				
			}
			Else 
			{
				Write-Out -ForegroundColor $global:colorWarn -Text "Destination EXIST (Do Not Overwrite): '$($RomDestination)'" -LogFile $LogFile
				Write-Out -ForegroundColor $global:colorWarn -Text "No Files Copied!'" -LogFile $LogFile
			}
		}
		Else 
		{
			Write-Out -ForegroundColor $global:colorSub1 -Text "Destination Not Exist: '$($RomDestination)'" -LogFile $LogFile
			Write-Out -ForegroundColor $global:colorSub1 -Text "Create Folder: '$($RomDestination)'" -LogFile $LogFile
			If (New-Item -ItemType Directory -Path $RomDestination -Force)
			{
				Write-Out -ForegroundColor $global:colorSub1 -Text "Copy $($colRoms.count) ROMs from '$($RomSource)' to '$($RomDestination)'" -LogFile $LogFile
				ForEach ($rom in $colRoms)
				{
					Copy-Item -LiteralPath $rom.FullName -Destination $RomDestination -Recurse -Force
				}
			}
			Else 
			{
				Write-Out -ForegroundColor $global:colorError -Text "ERROR Creating Destination No ROMs Copied!" -LogFile $LogFile
			}
		}
	}
	Else
	{
		Write-Out -ForegroundColor $global:colorError -Text "Source NOT Exist: '$($RomSource)'" -LogFile $LogFile
		Write-Out -ForegroundColor $global:colorError -Text "ERROR CANNOT COPY!!!" -LogFile $LogFile
	}
	#Optionally Unzip ROMs Incase .zip Not Supported By Emulator
	If ($romUnzip -eq $true)
	{
		Write-Out -ForegroundColor $global:colorSub1 -Text "Unzip ROMs In: '$($RomDestination)'" -LogFile $LogFile
		$colRoms = Get-ChildItem -LiteralPath $RomDestination -Filter "*.zip"
		ForEach ($rom in $colRoms)
		{
			Expand-Archive $rom.FullName -DestinationPath $RomDestination -Force
			Remove-Item -LiteralPath $rom.FullName -Force
		}
	}



	#Results
	Write-Out -ForegroundColor $global:colorMain -Text "$($GameSystem.ToUpper()) (STEP 6/6): Processing Results" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorMain -Text "================================================================" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROMs TOTAL              : $($countTotal)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROMs UNIQUE             : $($countUnique)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROMs Exceptions Deleted : $($countExceptionsDeleted)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROMs Dups Deleted       : $($countDuplicatesDeleted)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROMs Non-Dup Deleted    : $($countNonDuplicatesDeleted)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROMs Total Deleted      : $($countExceptionsDeleted + $countDuplicatesDeleted + $countNonDuplicatesDeleted)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "ROMs Total After Clean  : $($countTotal - ($countExceptionsDeleted + $countDuplicatesDeleted + $countNonDuplicatesDeleted))" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "Start Time              : $($timeStart)" -LogFile $LogFile
	$timeEnd = (Get-Date)
	Write-Out -ForegroundColor $global:colorSub1 -Text "End Time                : $($timeEnd)" -LogFile $LogFile
	Write-Out -ForegroundColor $global:colorSub1 -Text "Elapsed Time            : $([math]::Round(($timeEnd-$timeStart).totalseconds, 2)) seconds" -LogFile $LogFile
}



Function Get-NoIntroDefaultFilters
{
    Param (
        [Parameter(Mandatory=$false)]
        [string]
        $Name = "",
		[Parameter(Mandatory=$false)]
        [string]
        $Splitchar = "(",
        [Parameter(Mandatory=$false)]
        [string]
        $Unzip = '$false'
    )
    
    Begin
    {
        #Nothing to Do Here
    }
    
    Process
    {

        $DefaultFilters = @"
#$Name $global:scriptVersion
#No-Intro 2018 ROM Set

#Default Case Sensitive Character to Determine Unique ROM Name (Blank = Skip)
`$romSplitchar = "$Splitchar"

#Unzip ROMs That Do Not Support .ZIP Files On RetroPie
`$romUnzip = $Unzip
`n
"@        
        $DefaultFilters += @'
#Any Exact ROM Name (Including Extension) In This List Will Not Be Deleted
#-ROMs listed here will NOT be deleted even if they match a value in romsExceptions
#-ROMs listed here will NOT be deleted if it's NOT the Primary ROM via romsSortOrder
#-Ex: "Castlevania (USA) (Rev A).zip"
$romsDoNotDelete = @(
)

#Any ROMs Containing These Strings Or Exact ROM Name Will Be Removed Unless the Exact ROM Name Is In romsDoNotDelete
$romsExceptions = @(
    "[BIOS]"
    ,"[b]"
    ,"(Alpha)"
    ,"(Beta)"
    ,"(Beta 1)"
    ,"(Beta 2)"
    ,"(Beta 3)"
    ,"(Proto)"
    ,"(Proto 1)"
    ,"(Proto 2)"
    ,"(Promo)"
    ,"(Sample)"
    ,"(Debug Version)"
    ,"(Enhancement Chip)"
    ,"(GameCube Edition)"
    ,"(Promo, Virtual Console)"
    ,"(Virtual Console)"
    ,"(Wii Virtual Console)"
    ,"(Wii U Virtual Console)"
    ,"(Asia)"
    ,"(Australia)"
    ,"(Brazil)"
    ,"(Canada)"
    ,"(China)"
    ,"(France)"
    ,"(Germany)"
    ,"(Hong Kong)"
    ,"(Italy)"
    ,"(Japan)"
    ,"(Korea)"
    ,"(Netherlands)"
    ,"(Russia)"
    ,"(Spain)"
    ,"(Sweden)"
    ,"(Taiwan)"
)

#Preferred Sorting Order to Pick 1 Primary ROM to Keep When Evaluating Duplicates
#-Order Selects Specific Names followed by more generalized names based around language
#-ROMs in romsExceptions will not be deleted even if it is not determined as the primary
#-Ex: "Joust (USA).zip" would be primary over "Joust (Japan).zip"
$romsSortOrder = @(
    #---------------------------------------
    #---USA PREFERRED ORDER---
    #---------------------------------------
    #(USA).zip
    "\(USA\)\.zip"
    #(USA) (Unl).zip
    ,"\(USA\)\s*\(Unl\)\.zip"
    #(USA) (En,*).zip
    ,"\(USA\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(USA) (En,*) (Unl).zip
    ,"\(USA\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(USA) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(USA\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(USA, Europe).zip
    ,"\(USA,\s*Europe\)\.zip"
    #(USA, Europe) (Unl).zip
    ,"\(USA,\s*Europe\)\s*\(Unl\)\.zip"
    #(USA, Europe) (En,*).zip
    ,"\(USA,\s*Europe\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(USA, Europe) (En,*) (Unl).zip
    ,"\(USA,\s*Europe\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(USA, Europe) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(USA,\s*Europe\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(Japan, USA).zip
    ,"\(Japan,\s*USA\)\.zip"
    #(Japan, USA) (Unl).zip
    ,"\(Japan,\s*USA\)\s*\(Unl\)\.zip"
    #(Japan, USA) (En,*).zip
    ,"\(Japan,\s*USA\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(Japan, USA) (En,*) (Unl).zip
    ,"\(Japan,\s*USA\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(Japan, USA) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(Japan,\s*USA\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(USA, Asia).zip
    ,"\(USA,\s*Asia\)\.zip"
    #(USA, Asia) (Unl).zip
    ,"\(USA,\s*Asia\)\s*\(Unl\)\.zip"
    #(USA, Asia) (En,*).zip
    ,"\(USA,\s*Asia\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(USA, Asia) (En,*) (Unl).zip
    ,"\(USA,\s*Asia\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(USA, Asia) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(USA,\s*Asia\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"    
    #(USA, Australia).zip
    ,"\(USA,\s*Australia\)\.zip"
    #(USA, Australia) (Unl).zip
    ,"\(USA,\s*Australia\)\s*\(Unl\)\.zip"
    #(USA, Australia) (En,*).zip
    ,"\(USA,\s*Australia\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(USA, Australia) (En,*) (Unl).zip
    ,"\(USA,\s*Australia\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(USA, Australia) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(USA,\s*Australia\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(USA, Korea).zip
    ,"\(USA,\s*Korea\)\.zip"
    #(USA, Korea) (Unl).zip
    ,"\(USA,\s*Korea\)\s*\(Unl\)\.zip"
    #(USA, Korea) (En,*).zip
    ,"\(USA,\s*Korea\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(USA, Korea) (En,*) (Unl).zip
    ,"\(USA,\s*Korea\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(USA, Korea) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(USA,\s*Korea\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(USA) (Rev #)
    ,"^(?=.*?\(USA\))(?=.*?\(Rev\s*[0-9\.]{0,4}\)).*$"
    #(USA) (Rev A)
    ,"^(?=.*?\(USA\))(?=.*?\(Rev\s*[A-Z]{0,4}\)).*$"
    #(USA) (Rev #A)
    ,"^(?=.*?\(USA\))(?=.*?\(Rev\s*[A-Z0-9\.]{0,4}\)).*$"
    #(USA) (Alt) OR (Alternate)
    ,"^(?=.*?\(USA\))(?=.*?(\(Alt\)|\(Alternate\))).*$"
    #(USA) (Alt #)
    ,"^(?=.*?\(USA\))(?=.*?\(Alt\s*[A-Z0-9]{1,2}\)).*$"
    #(USA)
    ,"\(USA\)"
    #(USA, Europe)
    ,"\(USA,\s*Europe\)"
    #(Japan, USA)
    ,"\(Japan,\s*USA\)"
    #(USA, Asia)
    ,"\(USA,\s*Asia\)"
    #(USA, Australia)
    ,"\(USA,\s*Australia\)"
    #(USA, Korea)
    ,"\(USA,\s*Korea\)"    
    #(*USA*)
    ,"\((.*?)USA(.*?)\)"
    #---------------------------------------
    #---WORLD PREFERRED ORDER---
    #---------------------------------------
    #(World).zip
    "\(World\)\.zip"
    #(World) (Unl).zip
    ,"\(World\)\s*\(Unl\)\.zip"
    #(World) (En,*).zip
    ,"\(World\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(World) (En,*) (Unl).zip
    ,"\(World\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(World) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(World\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(World) (Rev #)
    ,"^(?=.*?\(World\))(?=.*?\(Rev\s*[0-9\.]{0,4}\)).*$"
    #(World) (Rev A)
    ,"^(?=.*?\(World\))(?=.*?\(Rev\s*[A-Z]{0,4}\)).*$"
    #(World) (Rev #A)
    ,"^(?=.*?\(World\))(?=.*?\(Rev\s*[A-Z0-9\.]{0,4}\)).*$"
    #(World) (Alt) OR (Alternate)
    ,"^(?=.*?\(World\))(?=.*?(\(Alt\)|\(Alternate\))).*$"
    #(World) (Alt #)
    ,"^(?=.*?\(World\))(?=.*?\(Alt\s*[A-Z0-9]{1,2}\)).*$"
    #(World)
    ,"\(World\)"
    #(*World*)
    ,"\((.*?)World(.*?)\)"
    #---------------------------------------
    #---EUROPE PREFERRED ORDER---
    #---------------------------------------
    #(Europe).zip
    "\(Europe\)\.zip"
    #(Europe) (Unl).zip
    ,"\(Europe\)\s*\(Unl\)\.zip"
    #(Europe) (En,*).zip
    ,"\(Europe\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(Europe) (En,*) (Unl).zip
    ,"\(Europe\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(Europe) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(Europe\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(Japan, Europe).zip
    ,"\(Japan,\s*Europe\)\.zip"
    #(Japan, Europe) (Unl).zip
    ,"\(Japan,\s*Europe\)\s*\(Unl\)\.zip"
    #(Japan, Europe) (En,*).zip
    ,"\(Japan,\s*Europe\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(Japan, Europe) (En,*) (Unl).zip
    ,"\(Japan,\s*Europe\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(Japan, Europe) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(Japan,\s*Europe\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(Europe) (Rev #)
    ,"^(?=.*?\(Europe\))(?=.*?\(Rev\s*[0-9\.]{0,4}\)).*$"
    #(Europe) (Rev A)
    ,"^(?=.*?\(Europe\))(?=.*?\(Rev\s*[A-Z]{0,4}\)).*$"
    #(Europe) (Rev #A)
    ,"^(?=.*?\(Europe\))(?=.*?\(Rev\s*[A-Z0-9\.]{0,4}\)).*$"
    #(Europe) (Alt) OR (Alternate)
    ,"^(?=.*?\(Europe\))(?=.*?(\(Alt\)|\(Alternate\))).*$"
    #(Europe) (Alt #)
    ,"^(?=.*?\(Europe\))(?=.*?\(Alt\s*[A-Z0-9]{1,2}\)).*$"
    #(Europe)
    ,"\(Europe\)"
    #(*Europe*)
    ,"\((.*?)Europe(.*?)\)"
    #---------------------------------------
    #---JAPAN PREFERRED ORDER---
    #---------------------------------------
    #(Japan).zip
    "\(Japan\)\.zip"
    #(Japan) (Unl).zip
    ,"\(Japan\)\s*\(Unl\)\.zip"
    #(Japan) (En,*).zip
    ,"\(Japan\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(Japan) (En,*) (Unl).zip
    ,"\(Japan\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(Japan) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(Japan\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(Japan, Korea).zip
    ,"\(Japan,\s*Korea\)\.zip"
    #(Japan, Korea) (Unl).zip
    ,"\(Japan,\s*Korea\)\s*\(Unl\)\.zip"
    #(Japan, Korea) (En,*).zip
    ,"\(Japan,\s*Korea\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\.zip"
    #(Japan, Korea) (En,*) (Unl).zip
    ,"\(Japan,\s*Korea\)\s*\(En(,|\+|Ca|Da|De|En|Es|Fi|Fr|It|Ja|Nl|No|Pl|Pt|Sv|Zh)*?\)\s*\(Unl\)\.zip"
    #(Japan, Korea) <Anything Except (Alt ) or (Rev ) or (Demo)>.zip
    ,"^(?=.*?\(Japan,\s*Korea\))((?!\(Alt\s*[A-Z0-9]{0,2}\)|\(Rev\s*[A-Z0-9\.]{0,4}\)|\(Demo\)).)*$"
    #(Japan) (Rev #)
    ,"^(?=.*?\(Japan\))(?=.*?\(Rev\s*[0-9\.]{0,4}\)).*$"
    #(Japan) (Rev A)
    ,"^(?=.*?\(Japan\))(?=.*?\(Rev\s*[A-Z]{0,4}\)).*$"
    #(Japan) (Rev #A)
    ,"^(?=.*?\(Japan\))(?=.*?\(Rev\s*[A-Z0-9\.]{0,4}\)).*$"
    #(Japan) (Alt) OR (Alternate)
    ,"^(?=.*?\(Japan\))(?=.*?(\(Alt\)|\(Alternate\))).*$"
    #(Japan) (Alt #)
    ,"^(?=.*?\(Japan\))(?=.*?\(Alt\s*[A-Z0-9]{1,2}\)).*$"
    #(Japan)
    ,"\(Japan\)"
    #(*Japan*)
    ,"\((.*?)Japan(.*?)\)"
    #---------------------------------------
    #---ALL ELSE FAILED PREFERRED ORDER---
    #---------------------------------------
    #(Unknown)
    ,"\(Unknown\)"
)
'@

        Return $DefaultFilters
    }
    
    End
    {
        #Nothing to Do Here
    }
}



Function Write-Out
{
	<#
	.SYNOPSIS
	Write output to console and/or log file. Supports multiple foreground and background colors on a single line and 
	advanced options to insert lines, spaces, or tabs before or after the text as well as timestamps.
	.DESCRIPTION
	Uses Write-Host to output color text to the console, and supports using multiple foreground and background colors 
	on a single line. Uses Set-Content and Add-Content to output to a file.
	
	It works by accepting strings as an array and then you can assign the foreground or background colors by passing
	them as arrays for the matching text. It also accepts Default which allows you skip specifying a color and use the 
	default.
	
	This function requires Powershell 4.0 to support the alias attribute in the function.
	.PARAMETER Text
	Optional. Array of strings to write to console/log. If you do not need to use multiple colors on a single line you can pass a single string instead of an array of strings. Otherwise, if you need to multiple colors on a single line pass an array of strings and associated colors using -ForegroundColor, -BackgroundColor, -ForegroundColorDefault, or -BackgroundColorDefault.
	.PARAMETER ForegroundColor
	Optional. Array of foreground Colors to write to console. Default means the ForegroundColorDefault value is used.
	Valid Colors: Default, Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
	.PARAMETER ForegroundColorDefault
	Optional. Default foreground color to write to console. Default is set to white.
	Valid Colors: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
	.PARAMETER BackgroundColor
	Optional. Array of background colors to write to console. Default means the BackgroundColorDefault value is used.
	Valid Colors: Default, Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
	.PARAMETER BackgroundColorDefault
	Optional. Default background color to write to console. Default is none.
	Valid Colors: Default, Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
	.PARAMETER PreLine
	Optional. Add blank lines before your text. Default is 0.
	.PARAMETER PreSpace
	Optional. Add spaces before your text. Default is 0.
	.PARAMETER PreTab
	Optional. Add Tabs before your text. Default is 0.
	.PARAMETER TimeStampFormat
	Optional. Format used for time stamps. Default is 'yyyy-MM-dd HH:mm:ss'
	.PARAMETER TimeStamp
	Optional. Adds time stamp in square brackets before your text.
	.PARAMETER ClearHost
	Optional. Clear the console before your text.
	.PARAMETER PostLine
	Optional. Add blank lines after your text. Default is 0.
	.PARAMETER PostSpace
	Optional. Add spaces after your text. Default is 0.
	.PARAMETER PostTab
	Optional. Add tabs after your text. Default is 0.
	.PARAMETER NoNewLine
	Optional. Do not add a new line after your text and optional post text options. Default is to add a new line.
	.PARAMETER LogFile
	Optional. Absolute or relative path of the log file.
	.PARAMETER Overwrite
	Optional. Ovewrite the log file. Default is to append to the log file.
	.INPUTS
	Parameters above
	.OUTPUTS
	None
	.NOTES
	Author: Brian Steinmeyer
	URL: http://sigkillit.com/
	Created: 4/18/2020
	Version 1.3
	- Added LogOnly Option for only writing text to a log file
	Version 1.2
	- Set Text, ForegroundColor, and BackgroundColor to default value of @() to fix errors checking counts in some circumstances
	- Fixed an issue where ForegroundColorDefault and BackgroundColorDefault were not working properly in some circumstances
	- Added Requires -Version 4.0
	Version 1.1
	- Completely rewrote the "Main Text" section
	- Added "Default" as a color option, which allows you to use the default values for foreground/background 
	  - Useful when you want to specify a backgroundcolor in certain parts of a line like the middle
	  - Ex: Write-Out -Text "How to ","highlight ","the middle text" -BackgroundColor Default,Yellow,Default
	Version: 1.0
	- Initial Creation inspired by PSWriteColor (https://github.com/EvotecIT/PSWriteColor)
	  - Improved upon by switching Foreground and Background Colors to default values if colors are not specifid for all strings.
	    Will also ignore extra colors if more colors are specified than strings specified.
	.EXAMPLE
	Write-Out -Text "Start with Red Text ","Then Switch to Blue Text ","Now Magenta" -ForegroundColor Red,Blue,Magenta 
	.EXAMPLE
	Write-Out -Text "White on Black ","Black on White ","Dark Cyan on Cyan ","Yellow on Green ","Default Color" -ForegroundColor White,Black,DarkCyan,Yellow -BackgroundColor Black,White,Cyan,Green
	.EXAMPLE
	Write-Out -Text "Make this"," entire line"," the same color by setting defaults" -ForegroundColorDefault Yellow -BackgroundColorDefault Magenta
	.EXAMPLE
	Write-Out -Text "Add a blank line and two tabs ","before ","my text" -ForegroundColor Green,Cyan,White -PreLine 1 -PreTab 2
	.EXAMPLE
	Write-Out -Text "Add two blank ","lines ","after my text" -ForegroundColor White,Green,White -PostLine 2
	.EXAMPLE
	Write-Out -Text "Add 3 spaces before my text" -ForegroundColor Gray -Prespace 3
	.EXAMPLE
	Write-Out -Text "White text and a tab after" -ForegroundColor White -NoNewLine -PostTab 1
	Write-Out -Text "Black text on Yellow ","and then back to white" -ForegroundColor Black,White -BackgroundColor Yellow
	.EXAMPLE
	Write-Out -Text "An easy way to ","highlight ","text in the middle" -BackgroundColor Default,Yellow
	.EXAMPLE
	Write-Out -Text "You can even add a ","time stamp ","before your output" -ForegroundColor White,Green,White -TimeStamp -PreLine 3
	.EXAMPLE
	Write-Out -Text "You can change the ","time stamp format" -ForegroundColor White,Yellow -TimeStamp -TimeStampFormat "dd-MM-yyy HH:mm" -PreLine 1 -PostLine 1
	.EXAMPLE
	Write-Out -Text "An"," Error"," occurred let's write overwrite/create a new log file" -ForegroundColor White,Red,White -TimeStamp -LogFile "script.log" -Overwrite
	.EXAMPLE
	Write-Out -Text "Now you can ","Append ","this line to your log file" -ForegroundColor Cyan,Magenta -TimeStamp -LogFile "script.log"
	#>
	[CmdletBinding()]
	Param (
		[Parameter(Mandatory=$false)]
		[AllowEmptyString()]
        [alias ('T')]
		[String[]]
		$Text = @(),
		[Parameter(Mandatory=$false)]
		[ValidateSet("Default","Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")]
		[alias ('FGC', 'FC')]
		[string[]]
		$ForegroundColor = @(),
		[Parameter(Mandatory=$false)]
		[ValidateSet("Default","Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")]
		[alias ('FGCD', 'FCD')]
		[string]
		$ForegroundColorDefault = [ConsoleColor]::White,
		[Parameter(Mandatory=$false)]
        [ValidateSet("Default","Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")]
		[alias ('BGC', 'BC')]
		[String[]]
		$BackgroundColor = @(),
		[Parameter(Mandatory=$false)]
		[ValidateSet("Default","Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")]
		[alias ('BGCD', 'BCD')]
		[string]
		$BackgroundColorDefault = "Default",
		[Parameter(Mandatory=$false)]
		[int]
		$PreLine = 0,
		[Parameter(Mandatory=$false)]
		[int]
		$PreSpace = 0,
		[Parameter(Mandatory=$false)]
		[int]
		$PreTab = 0,
		[Parameter(Mandatory=$false)]
		[Alias('TSF', 'TS')]
		[string]
		$TimeStampFormat = 'yyyy-MM-dd HH:mm:ss',
		[Parameter(Mandatory=$false)]
		[switch]
		$TimeStamp,		
		[Parameter(Mandatory=$false)]
		[switch]
		$ClearHost,
		[Parameter(Mandatory=$false)]
		[int]
		$PostLine = 0,
		[Parameter(Mandatory=$false)]
		[int]
		$PostSpace = 0,
		[Parameter(Mandatory=$false)]
		[int]
		$PostTab = 0,
		[Parameter(Mandatory=$false)]
		[switch]
		$NoNewLine = $false,
		[Parameter(Mandatory=$false)]
		[alias ('Log', 'L')]
		[string]
		$LogFile = '',
		[Parameter(Mandatory=$false)]
		[switch]
		$LogOnly = $false,
		[Parameter(Mandatory=$false)]
		[switch]
		$Overwrite = $false
	)
	
	Begin
	{
		#Nothing to Do Here
	}
	
	Process
	{
		#Optional - Do Not Write to ConsoleColor
		If (!($LogOnly))
		{
			#Optional - Prefix Text 
			If ($ClearHost) { Clear-Host }
			If ($PreLine -gt 0) { For ($i = 0; $i -lt $PreLine; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line(s) before Main text
			If ($PreSpace -gt 0) { For ($i = 0; $i -lt $PreSpace; $i++) { Write-Host -Object " " -NoNewLine } }  # Add Tab(s) before Main text
			If ($PreTab -gt 0) { For ($i = 0; $i -lt $PreTab; $i++) { Write-Host -Object "`t" -NoNewLine } }  # Add Space(s) before Main text
			If ($TimeStamp) { Write-Host -Object "[$([datetime]::Now.ToString($TimeStampFormat))]" -NoNewline } # Add Timestamp before Main Text

			#MAIN TEXT
			If (($Text.Count -gt 0) -AND ($ForegroundColor.Count -eq 0) -AND ($BackgroundColor.Count -eq 0))
			{
				#Text Only Specified
				For ($i = 0; $i -lt $Text.Count; $i++)
				{
					If ($BackgroundColorDefault -eq "Default")
					{
						Write-Host $Text[$i] -ForegroundColor $ForegroundColorDefault -NoNewLine
					}
					Else 
					{
						Write-Host $Text[$i] -ForegroundColor $ForegroundColorDefault -BackgroundColor $BackgroundColorDefault -NoNewLine
					}
				} 
			}
			ElseIf (($Text.Count -gt 0) -AND ($ForegroundColor.Count -gt 0) -AND ($BackgroundColor.Count -eq 0))
			{ 
				#Text and ForegroundColor Specified
				For ($i = 0; $i -lt $Text.Count; $i++)
				{
					If ($ForegroundColor.Count -le $i) { $ForegroundColor += $ForegroundColorDefault } #ForegroundColor Not Specified Set to Default
					If ($ForegroundColor[$i] -eq "Default") { $ForegroundColor[$i] = $ForegroundColorDefault }
					If ($BackgroundColorDefault -eq "Default")
					{
						Write-Host $Text[$i] -ForegroundColor $ForegroundColor[$i] -NoNewLine
					}
					Else 
					{
						Write-Host $Text[$i] -ForegroundColor $ForegroundColor[$i] -BackgroundColor $BackgroundColorDefault -NoNewLine
					}
				}
			}
			ElseIf (($Text.Count -gt 0) -AND ($ForegroundColor.Count -eq 0) -AND ($BackgroundColor.Count -gt 0))
			{
				#Text and BackgroundColor Specified
				For ($i = 0; $i -lt $Text.Count; $i++)
				{
					If ($BackgroundColor.Count -le $i) { $BackgroundColor += $BackgroundColorDefault } #BackgroundColor Not Specified Set to Default
					If ($BackgroundColor[$i] -eq "Default")
					{
						If ($BackgroundColorDefault -eq "Default")
						{
							Write-Host $Text[$i] -ForegroundColor $ForegroundColorDefault -NoNewLine
						}
						Else
						{
							Write-Host $Text[$i] -ForegroundColor $ForegroundColorDefault -BackgroundColor $BackgroundColorDefault -NoNewLine
						}
					}
					Else 
					{
						Write-Host $Text[$i] -ForegroundColor $ForegroundColorDefault -BackgroundColor $BackgroundColor[$i] -NoNewLine
					}
				}
			}
			ElseIf (($Text.Count -gt 0) -AND ($ForegroundColor.Count -gt 0) -AND ($BackgroundColor.Count -gt 0))
			{ 
				#Text, ForegroundColor, and BackgroundColor Specified (FAILS NOT WRITING DEFAULT)
				For ($i = 0; $i -lt $Text.Count; $i++)
				{
					If ($ForegroundColor.Count -le $i) { $ForegroundColor += $ForegroundColorDefault } #ForegroundColor Not Specified Set to Default
					If ($BackgroundColor.Count -le $i) { $BackgroundColor += $BackgroundColorDefault } #BackgroundColor Not Specified Set to Default
					If ($ForegroundColor[$i] -eq "Default") { $ForegroundColor[$i] = $ForegroundColorDefault }
					If ($BackgroundColor[$i] -eq "Default")
					{
						If ($BackgroundColorDefault -eq "Default")
						{
							Write-Host $Text[$i] -ForegroundColor $ForegroundColor[$i] -NoNewLine
						}
						Else
						{
							Write-Host $Text[$i] -ForegroundColor $ForegroundColor[$i] -BackgroundColor $BackgroundColorDefault -NoNewLine
						}					
					}
					Else 
					{
						Write-Host $Text[$i] -ForegroundColor $ForegroundColor[$i] -BackgroundColor $BackgroundColor[$i] -NoNewLine
					}
				}
			}
			Else
			{
				#No Text, ForegroundColor, or BackgroundColor Specified ($Text.Count -eq 0) -AND ($ForegroundColor.Count -eq 0) -AND ($BackgroundColor.Count -eq 0)
			}
				
			#Post Text
			If ($PostSpace -gt 0) { for ($i = 0; $i -lt $PostSpace; $i++) { Write-Host -Object " " -NoNewLine } }  # Add Tab(s) after Main text
			If ($PostTab -gt 0) { for ($i = 0; $i -lt $PostTab; $i++) { Write-Host -Object "`t" -NoNewLine } }  # Add Space(s) after Main text
			If ($PostLine -gt 0) { for ($i = 0; $i -lt $PostLine; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line(s) after Main text
			If ($NoNewLine) { Write-Host -NoNewline } else { Write-Host } # Add New Line after Main Text Unless NoNewLine is Supplied
		}
		
		#Log File
		$TextToWrite = "" #Build Text to Write to Log File
		If ($PreLine -gt 0) { For ($i = 0; $i -lt $PreLine; $i++) { $TextToWrite += "`n" } } # Add empty line(s) before Main text
		If ($PreSpace -gt 0) { For ($i = 0; $i -lt $PreSpace; $i++) { $TextToWrite += " " } }  # Add Tab(s) before Main text
		If ($PreTab -gt 0) { For ($i = 0; $i -lt $PreTab; $i++) { $TextToWrite += "`t" } }  # Add Space(s) before Main text
		If ($TimeStamp) { $TextToWrite += "[$([datetime]::Now.ToString($TimeStampFormat))]" } # Add Timestamp before Main Text
		If ($Text.Count -gt 0) { For ($i = 0; $i -lt $Text.Count; $i++) { $TextToWrite += $Text[$i] } } #Add Main Text
		If ($PostSpace -gt 0) { for ($i = 0; $i -lt $PostSpace; $i++) { $TextToWrite += " " } }  # Add Tab(s) after Main text
		If ($PostTab -gt 0) { for ($i = 0; $i -lt $PostTab; $i++) { $TextToWrite += "`t" } }  # Add Space(s) after Main text
		If ($PostLine -gt 0) { for ($i = 0; $i -lt $PostLine; $i++) { $TextToWrite += "`n" } } # Add empty line(s) after Main text		
		If ($LogFile -eq "")
		{
			#No LogFile Specified - Skip
		}
		ElseIf(!(Test-Path -Path $LogFile))
		{
			#Create Log File If Does Not Exist
			If ($NoNewLine)
			{
				$nf = New-Item -Path $LogFile -type file -force -Value $TextToWrite
			}
			Else 
			{
				$nf = New-Item -Path $LogFile -type file -force -Value $TextToWrite`r`n
			}
		}
		Else
		{
			#Log File Exists			
			If($Overwrite)
			{
				#Overwrite Log File
				If ($NoNewLine)
				{
					Set-Content -Path $LogFile -Value $TextToWrite -NoNewline
				}
				Else 
				{
					Set-Content -Path $LogFile -Value $TextToWrite
				}
			}
			Else
			{
				#Append Log File
				Try {
					If ($NoNewLine)
					{
						Add-Content -Path $LogFile -Value $TextToWrite -ErrorAction Stop -NoNewline
					}
					Else 
					{
						Add-Content -Path $LogFile -Value $TextToWrite -ErrorAction Stop
					}
				} Catch {
					Start-Sleep -s 3
					If ($NoNewLine)
					{
						Add-Content -Path $LogFile -Value $TextToWrite -NoNewline
					}
					Else 
					{
						Add-Content -Path $LogFile -Value $TextToWrite
					}
				}
			}
		}
	}
	
	End
	{
		#Nothing to Do Here
	}
}



Main
Aug 31

Remove RDS CALs from RDS Server

Background

There are many circumstances where you will need to remove a RDS CALs from an RDS Server, or in some cases you want to rebuild the entire RD licensing database.  Microsoft allows you to remove an individual CAL license pack using powershell, or rebuild the entire database.  However, if neither of those work, it’s quite easy to manually rebuild the RD licensing database.  I’ve included directions for all 3 methods below, and have tested this on Windows Server 2008, 2008R2, 2012, 2012R2, and 2016.

Remove An Individual RDS CAL License Pack Using Powershell (User or Device CAL)

  • Open powershell elevated as an administrator
  • Type the following command to list the RDS Licenses and note the KeyPackID
    • Alternatively, open RD Licensing Manager and note the Keypack ID
Get-WmiObject Win32_TSLicenseKeyPack
  • Run the below command to remove the licenses pack from your RD Server
    • Replace KEYPACKID with the number you obtained above
wmic /namespace:\\root\CIMV2 PATH Win32_TSLicenseKeyPack CALL UninstallLicenseKeyPackWithId KEYPACKID

 

Rebuild the RD Licensing Database

Microsoft provides directions on how to do this automatically, via a web browser, or via the phone:

https://technet.microsoft.com/en-us/library/dd851428.aspx

 

Manually Rebuild the Licensing Database (Guaranteed to Work if the Previous 2 methods Fail)

  • Make sure you have documentation of your MS License agreement that includes Authorization number, License number, License type (User/Device CAL), and Quantity before proceeding
  • Stop the Remote Desktop Licensing service

Stop Remote Desktop Licensing Service

  • Rename C:\Windows\System32\lserver\TLSLic.edb to C:\Windows\System32\lserver\TLSLic.old
  • Start the Remote Desktop Licensing service
  • All licenses will now be cleared out of RD Licensing Manager, and you’ll need to re-install the licenses you want to add back in
Oct 28

Missing Power and Search Button On Start Screen

Missing Power and Search Button On Start Screen on Server 2012 R2 and Windows 8.1

Recently, after installing a Windows 2012 R2 Server VM, I noticed the Start screen was missing the Power and Search buttons.  All of my older Server 2012 R2 Server VM’s had the Power and Search button on the start screen, I used the same ISO to install, and Windows was 100% up to date.  After researching, this issue can affect Windows Server 2012 R2 as well as Windows 8.1.  Here is a screenshot of my Start screen missing both the Power and Search buttons:

Power and Search Button Missing On Start Screen

The Solution

The short answer is KB2919355 needs installed.  HOWEVER, you need to make sure you install the following updates in this order:

  1. Prerequisite: KB2919442
  2. Install KB2919355 that has multiple downloads in it, which can be directly downloaded from the Microsoft Download Center
    1. Windows 8.1 x86
    2. Windows 8.1 x64
    3. Server 2012 R2 x64
    4. NOTE: Once downloaded, these updates need installed in the following order
      1. clearcompressionflag.exe
      2. KB2919355 (Reboot after installation, if fixed you can skip the rest)
      3. KB2932046
      4. KB2959977
      5. KB2937592
      6. KB2938439
      7. KB2934018

Once KB2919355 is installed and you reboot, you should now see the Power and Search buttons on the start screen as displayed below:

Power and Search Button On Start Screen

Final Notes

Once KB2919355 is installed, run Windows Updates and you’ll probably need to install a few hundred updates again

 

 

Sep 22

Move Off-Screen Window in Windows

Have you ever had a window that ended up off of your screen and were unable to move it back into view?  I’ve personally had this occur after disconnecting a monitor from my laptop which I had my desktop extended on.  However, there’s a simple trick to move the Window back into view of your desktop.

Make the Off-Screen Window Active

  • Hold the Alt key down and then press Tab key until you’ve selected the Window that is off-screen

tab_selector

Select Move for the Active Window

  • Hold the Alt key down, press the Space-bar key, then press the M key

Move Your Window Back Into View

  • You can now move the window in the desired direction with the arrow keys
Jul 14

Complex Password Generator

Function Get-RandomPassword {
     param(
         $length = 10,
         $characters = 'abcdefghkmnprstuvwxyzABCDEFGHKLMNPRSTUVWXYZ123456789!"??$%&/()=?*+#_'
     )
     $random = 1..$length | ForEach-Object { Get-Random -Maximum $characters.length }
     $private:ofs=""
     [String]$characters[$random]
 }
 Function Randomize-Text {
     param(
         $text
     )
     $number = $text.length -1
     $indexes = Get-Random -InputObject (0..$number) -Count $number
     $private:ofs=''
     [String]$text[$indexes]
 }
 Function Get-ComplexPassword {
     $password = Get-RandomPassword -length 8 -characters 'abcdefghiklmnprstuvwxyz'
     $password += Get-RandomPassword -length 2 -characters '#*+)'
     $password += Get-RandomPassword -length 4 -characters '123456789'
     $password += Get-RandomPassword -length 6 -characters 'ABCDEFGHKLMNPRSTUVWXYZ'
     Randomize-Text $password
 }
 Get-ComplexPassword
Jul 14

365 Password Generator

This powershell script bulk generates passwords in a similar style as the password generator in Office 365.  The passwords begin with a capital letter, followed by 5 lower case letters, and 2 digits at the end.  You can modify the pattern to suite your needs (Note: It’s using the ASCII table ranges as the set it randomly chooses from).

Function Generate365PW
{
	Param($max = 1)
	For ($i = 0; $i -lt $max; $i++){
		$pw = Get-Random -Count 1 -InputObject ((65..72)+(74..75)+(77..78)+(80..90)) | % -begin {$UC=$null} -process {$UC += [char]$_} -end {$UC}
		$pw += Get-Random -Count 5 -InputObject (97..122) | % -begin {$LC=$null} -process {$LC += [char]$_} -end {$LC}
		$pw += Get-Random -Count 2 -InputObject (48..57) | % -begin {$NB=$null} -process {$NB += [char]$_} -end {$NB}
		write-output $pw
	}
}
Generate365PW 10
Mar 03

Determine If Distribution Group is Being Used in 365 Exchange

“What distribution groups are in use?” and “How many emails are sent to a specific distribution group per month?” are common questions I receive with 365 Exchange or Exchange.  Unfortunately, there is nothing built in that tracks how many emails on sent to a distribution group.  However we can use Get-MessageTrace to count the number of messages sent to a distribution group for a time range with the max being 30 days.  Also note, the by default PageSize returns 1000 items but you can increase the PageSize to 5000 items.  For example, to get the number of emails sent to the distribution group everyone@domain.com for a single day we can use:

$DGCount = Get-MessageTrace -PageSize 5000 -RecipientAddress "everyone@domain.com" -StartDate ([DateTime]::Today.AddDays(-1)) -EndDate ([DateTime]::Today) | ForEach-Object {$count++}
$DGCount

Using this method, we can count the number of emails sent to each distribution group each day and store the results in an output file.  We can then query those output files and create a report.  In my example, the report will show the total emails sent to each distribution group by month and go back 12 months.  Now without further ado, let’s get to the two scripts needed.

365_DGCounter.ps1

#365_DGCounter.ps1
# ------ SCRIPT CONFIGURATION ------
#Log File
$LogFile = $MyInvocation.MyCommand.Path.Replace($MyInvocation.MyCommand.Name,"Logs\") + ((Get-Date).AddDays(-1).ToString('yyyy_MM_dd')) + ".xml"
#Table Name
$TableName = "DG_Emails_Received"
# ------ END CONFIGURATION ------

#Confirm Logs Directory Exists
$LogPath = $MyInvocation.MyCommand.Path.Replace($MyInvocation.MyCommand.Name,"Logs\")
If (!(Test-Path $LogPath)){
	New-Item -ItemType Directory -Force -Path $LogPath
}

#Import Modules and Connect to Office 365
import-module ActiveDirectory
Try {
	$UserCredential = Get-Credential
	$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $UserCredential -Authentication Basic -AllowRedirection
	Import-PSSession $Session
}
Catch {
	#Error Connecting to Office365
}

#Create Data Table
$CounterTable = New-Object system.Data.DataTable $TableName
$CounterCol1 = New-Object system.Data.DataColumn Date,([datetime])
$CounterCol2 = New-Object system.Data.DataColumn Email,([string])
$CounterCol3 = New-Object system.Data.DataColumn Count,([int])
$CounterTable.columns.add($CounterCol1)
$CounterTable.columns.add($CounterCol2)
$CounterTable.columns.add($CounterCol3)

#Get DG's and Add to Table
$DG = Get-DistributionGroup -Resultsize Unlimited | select Displayname, Primarysmtpaddress
$DG | ForEach-Object {
	$count = 0
	#Note: [DateTime]::Today returns 12:00 AM of the Current Date 
	#      Let's assume the date is currently 3/3/2016, the below returns the range 3/2/2016 12:00 AM to 3/3/2016 12:00 AM
	Get-MessageTrace -PageSize 5000 -RecipientAddress $_.Primarysmtpaddress -StartDate ([DateTime]::Today.AddDays(-1)) -EndDate ([DateTime]::Today) | ForEach-Object {$count++}
	$CounterRow = $CounterTable.NewRow()
	$CounterRow.Date = [datetime](Get-Date).AddDays(-1)
	$CounterRow.Email = $_.Primarysmtpaddress
	$CounterRow.Count = $count
	$CounterTable.Rows.Add($CounterRow)
}

#Output Results
$CounterTable.WriteXml($LogFile)
$CounterTable.WriteXmlSchema($LogFile.replace(".xml",".xsd"))
$CounterTable | format-table -AutoSize

#Disconnect From Office365
Remove-PSSession $Session

Note: [DateTime]::Today returns 12:00 AM of the Current Date.  Let’s assume the date is currently 3/3/2016.  The script above would return the range 3/2/2016 12:00 AM to 3/3/2016 12:00 AM.  Running this script will actually return the email counts from yesterday since today has not ended.

365_DGCounterReport.ps1

#365_DGCounterReport.ps1
# ------ SCRIPT CONFIGURATION ------
#Log File Path
$LogPath = $MyInvocation.MyCommand.Path.Replace($MyInvocation.MyCommand.Name,"Logs\")
#Table Names
$MasterTableName = "DG_Emails_Received"
$ResultTableName = "DG_Emails_Received"
# ------ END CONFIGURATION ------

#Import XML Files to Master Table
$MasterTable = New-Object system.Data.DataTable $MasterTableName
$MasterCol1 = New-Object system.Data.DataColumn Date,([datetime])
$MasterCol2 = New-Object system.Data.DataColumn Email,([string])
$MasterCol3 = New-Object system.Data.DataColumn Count,([int])
$MasterTable.columns.add($MasterCol1)
$MasterTable.columns.add($MasterCol2)
$MasterTable.columns.add($MasterCol3)
Get-ChildItem $LogPath -Filter "*.xml" | Sort-Object Name | ForEach-Object {
	$XmlDocument = [XML](Get-Content -Path $_.FullName)
	$XmlDocument.DocumentElement.$MasterTableName | ForEach-Object {
		$MasterRow = $MasterTable.NewRow()
		$MasterRow.Date = [datetime]$_.Date
		$MasterRow.Email = $_.Email
		$MasterRow.Count = [int]$_.Count
		$MasterTable.Rows.Add($MasterRow)
	}
}

#Get List of Unique Distribution Groups from Master Table
$DGs = @{}
$MasterTable | ForEach-Object {
	If (!($DGs.ContainsKey($_.Email))){
		$DGs.Add($_.Email,$_.Email)
	}
}
$DGs = $DGs.GetEnumerator() | Sort-Object Name

#Count Emails By Month
$ResultTable = New-Object system.Data.DataTable $ResultTableName
$ResultCol1 = New-Object system.Data.DataColumn Email,([string])
$ResultCol2 = New-Object system.Data.DataColumn Mon,([int])
$ResultCol3 = New-Object system.Data.DataColumn Mon_1,([int])
$ResultCol4 = New-Object system.Data.DataColumn Mon_2,([int])
$ResultCol5 = New-Object system.Data.DataColumn Mon_3,([int])
$ResultCol6 = New-Object system.Data.DataColumn Mon_4,([int])
$ResultCol7 = New-Object system.Data.DataColumn Mon_5,([int])
$ResultCol8 = New-Object system.Data.DataColumn Mon_6,([int])
$ResultCol9 = New-Object system.Data.DataColumn Mon_7,([int])
$ResultCol10 = New-Object system.Data.DataColumn Mon_8,([int])
$ResultCol11 = New-Object system.Data.DataColumn Mon_9,([int])
$ResultCol12 = New-Object system.Data.DataColumn Mon_10,([int])
$ResultCol13 = New-Object system.Data.DataColumn Mon_11,([int])
$ResultCol14 = New-Object system.Data.DataColumn Total,([int])
$ResultTable.columns.add($ResultCol1)
$ResultTable.columns.add($ResultCol2)
$ResultTable.columns.add($ResultCol3)
$ResultTable.columns.add($ResultCol4)
$ResultTable.columns.add($ResultCol5)
$ResultTable.columns.add($ResultCol6)
$ResultTable.columns.add($ResultCol7)
$ResultTable.columns.add($ResultCol8)
$ResultTable.columns.add($ResultCol9)
$ResultTable.columns.add($ResultCol10)
$ResultTable.columns.add($ResultCol11)
$ResultTable.columns.add($ResultCol12)
$ResultTable.columns.add($ResultCol13)
$ResultTable.columns.add($ResultCol14)
$CurDate = Get-Date
$DGs | ForEach-Object{
	$DG = $_.Name
	$ResultRow = $ResultTable.NewRow()
	$ResultRow.Email = $DG
	$ResultRow.Mon = 0
	$ResultRow.Mon_1 = 0
	$ResultRow.Mon_2 = 0
	$ResultRow.Mon_3 = 0
	$ResultRow.Mon_4 = 0
	$ResultRow.Mon_5 = 0
	$ResultRow.Mon_6 = 0
	$ResultRow.Mon_7 = 0
	$ResultRow.Mon_8 = 0
	$ResultRow.Mon_9 = 0
	$ResultRow.Mon_10 = 0
	$ResultRow.Mon_11 = 0
	$ResultRow.Total = 0
	$MasterTable | ForEach-Object {
		If ($DG -eq $_.Email){
			#Current Month
			If ($_.Date.Month -eq $CurDate.Month){
				$ResultRow.Mon += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-1
			If ($_.Date.Month -eq $CurDate.AddMonths(-1).Month){
				$ResultRow.Mon_1 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-2
			If ($_.Date.Month -eq $CurDate.AddMonths(-2).Month){
				$ResultRow.Mon_2 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-3
			If ($_.Date.Month -eq $CurDate.AddMonths(-3).Month){
				$ResultRow.Mon_3 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-4
			If ($_.Date.Month -eq $CurDate.AddMonths(-4).Month){
				$ResultRow.Mon_4 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-5
			If ($_.Date.Month -eq $CurDate.AddMonths(-5).Month){
				$ResultRow.Mon_5 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-6
			If ($_.Date.Month -eq $CurDate.AddMonths(-6).Month){
				$ResultRow.Mon_6 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-7
			If ($_.Date.Month -eq $CurDate.AddMonths(-7).Month){
				$ResultRow.Mon_7 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-8
			If ($_.Date.Month -eq $CurDate.AddMonths(-8).Month){
				$ResultRow.Mon_8 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-9
			If ($_.Date.Month -eq $CurDate.AddMonths(-9).Month){
				$ResultRow.Mon_9 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-10
			If ($_.Date.Month -eq $CurDate.AddMonths(-10).Month){
				$ResultRow.Mon_10 += $_.Count
				$ResultRow.Total += $_.Count
			}
			#Current Month-11
			If ($_.Date.Month -eq $CurDate.AddMonths(-11).Month){
				$ResultRow.Mon_11 += $_.Count
				$ResultRow.Total += $_.Count
			}
		}
	}
	$ResultTable.Rows.Add($ResultRow)
}

#Rewrite Column Names as Months
For ($i=0; $i -lt 12; $i++) {
	If ($i -eq 0){
		$ResultTable.columns["Mon"].ColumnName = "$((Get-Culture).DateTimeFormat.GetAbbreviatedMonthName($CurDate.Month)) $($CurDate.Year)"
	}Else{
		$ResultTable.columns["Mon_$($i)"].ColumnName = "$((Get-Culture).DateTimeFormat.GetAbbreviatedMonthName($CurDate.AddMonths($i * -1).Month)) $($CurDate.AddMonths($i * -1).Year)"
	}
}

#Output Results
$ResultTable | format-table -AutoSize
$ResultTable | Export-CSV Report.csv -notypeinformation