Files
SnootGame/game/python-packages/ost.py
2022-10-28 00:56:32 -03:00

668 lines
19 KiB
Python

# Copyright (C) 2021 GanstaKingofSA (Hanaka)
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Pardon any mess within this PY file. Finally PEP8'd it.
import random
import re
import os
import json
import renpy
import pygame_sdl2
from tinytag import TinyTag
from renpy.text.text import Text
from renpy.display.im import image
import renpy.audio.music as music
import renpy.display.behavior as displayBehavior
# Creation of Music Room and Code Setup
version = 1.6
music.register_channel("music_room", mixer="music_room_mixer", loop=False)
if renpy.windows:
gamedir = renpy.config.gamedir.replace("\\", "/")
elif renpy.android:
try: os.mkdir(os.path.join(os.environ["ANDROID_PUBLIC"], "game"))
except: pass
gamedir = os.path.join(os.environ["ANDROID_PUBLIC"], "game")
else:
gamedir = renpy.config.gamedir
# Lists for holding media types
autoDefineList = []
manualDefineList = []
soundtracks = []
file_types = ('.mp3', '.ogg', '.opus', '.wav')
# Stores soundtrack in progress
game_soundtrack = False
# Stores positions of track/volume/default priority
time_position = 0.0
time_duration = 3.0
old_volume = 0.0
priorityScan = 2
scale = 1.0
# Stores paused track/player controls
game_soundtrack_pause = False
prevTrack = False
randomSong = False
loopSong = False
organizeAZ = False
organizePriority = True
pausedstate = False
random.seed()
class soundtrack:
'''
Class responsible to define songs to the music player.
'''
def __init__(self, name="", path="", priority=2, author="", byteTime=False,
description="", cover_art=False, unlocked=True):
self.name = name
self.path = path
self.priority = priority
self.author = author
self.byteTime = byteTime
self.description = description
if not cover_art:
self.cover_art = "images/music_room/nocover.png"
else:
self.cover_art = cover_art
self.unlocked = unlocked
@renpy.exports.pure
class AdjustableAudioPositionValue(renpy.ui.BarValue):
'''
Class that replicates a music progress bar in Ren'Py.
'''
def __init__(self, channel='music_room', update_interval=0.0):
self.channel = channel
self.update_interval = update_interval
self.adjustment = None
self._hovered = False
def get_pos_duration(self):
if not music.is_playing(self.channel):
pos = time_position
else:
pos = music.get_pos(self.channel) or 0.0
duration = time_duration
return pos, duration
def get_song_options_status(self):
return loopSong, randomSong
def get_adjustment(self):
pos, duration = self.get_pos_duration()
self.adjustment = renpy.ui.adjustment(value=pos, range=duration,
changed=self.set_pos, adjustable=True)
return self.adjustment
def hovered(self):
self._hovered = True
def unhovered(self):
self._hovered = False
def set_pos(self, value):
loopThis = self.get_song_options_status()
if (self._hovered and pygame_sdl2.mouse.get_pressed()[0]):
music.play("<from {}>".format(value) + game_soundtrack.path,
self.channel)
if loopThis:
music.queue(game_soundtrack.path, self.channel, loop=True)
def periodic(self, st):
pos, duration = self.get_pos_duration()
loopThis, doRandom = self.get_song_options_status()
if pos and pos <= duration:
self.adjustment.set_range(duration)
self.adjustment.change(pos)
if pos > duration - 0.20:
if loopThis:
music.play(game_soundtrack.path, self.channel, loop=True)
elif doRandom:
random_song()
else:
next_track()
return self.update_interval
if renpy.config.screen_width != 1280:
scale = renpy.config.screen_width / 1280.0
else:
scale = 1.0
def music_pos(style_name, st, at):
'''
Returns the track position to Ren'Py.
'''
global time_position
if music.get_pos(channel='music_room') is not None:
time_position = music.get_pos(channel='music_room')
readableTime = convert_time(time_position)
d = Text(readableTime, style=style_name)
return d, 0.20
def music_dur(style_name, st, at):
'''
Returns the track duration to Ren'Py.
'''
global time_duration
if game_soundtrack.byteTime:
time_duration = game_soundtrack.byteTime
else:
time_duration = music.get_duration(
channel='music_room') or time_duration
readableDuration = convert_time(time_duration)
d = Text(readableDuration, style=style_name)
return d, 0.20
def dynamic_title_text(style_name, st, at):
'''
Returns a resized song title text to Ren'Py.
'''
title = len(game_soundtrack.name)
if title <= 21:
songNameSize = int(37 * scale)
elif title <= 28:
songNameSize = int(29 * scale)
else:
songNameSize = int(23 * scale)
d = Text(game_soundtrack.name, style=style_name, substitute=False,
size=songNameSize)
return d, 0.20
def dynamic_author_text(style_name, st, at):
'''
Returns a resized song artist text to Ren'Py.
'''
author = len(game_soundtrack.author)
if author <= 32:
authorNameSize = int(25 * scale)
elif author <= 48:
authorNameSize = int(23 * scale)
else:
authorNameSize = int(21 * scale)
d = Text(game_soundtrack.author, style=style_name, substitute=False,
size=authorNameSize)
return d, 0.20
def refresh_cover_data(st, at):
'''
Returns the song cover art to Ren'Py.
'''
d = image(game_soundtrack.cover_art)
return d, 0.20
def dynamic_description_text(style_name, st, at):
'''
Returns a resized song album/comment to Ren'Py.
'''
desc = len(game_soundtrack.description)
if desc <= 32:
descSize = int(25 * scale)
elif desc <= 48:
descSize = int(23 * scale)
else:
descSize = int(21 * scale)
d = Text(game_soundtrack.description, style=style_name, substitute=False,
size=descSize)
return d, 0.20
def auto_play_pause_button(st, at):
'''
Returns either a play/pause button to Ren'Py based off song play status.
'''
if music.is_playing(channel='music_room'):
if pausedstate:
d = renpy.display.behavior.ImageButton("images/music_room/pause.png")
else:
d = renpy.display.behavior.ImageButton("images/music_room/pause.png",
action=current_music_pause)
else:
d = displayBehavior.ImageButton("images/music_room/play.png",
action=current_music_play)
return d, 0.20
def rpa_mapping_detection(style_name, st, at):
'''
Returns a warning message to the player if it can't find the RPA cache
JSON file in the game folder.
'''
try:
renpy.exports.file("RPASongMetadata.json")
return Text("", size=23), 0.0
except:
return Text("{b}Warning:{/b} The RPA metadata file hasn't been generated. Songs in the {i}track{/i} folder that are archived into a RPA won't work without it. Set {i}config.developer{/i} to {i}True{/i} in order to generate this file.", style=style_name, size=20), 0.0
def convert_time(x):
'''
Converts track position and duration to human-readable time.
'''
hour = ""
if int (x / 3600) > 0:
hour = str(int(x / 3600))
if hour != "":
if int((x % 3600) / 60) < 10:
minute = ":0" + str(int((x % 3600) / 60))
else:
minute = ":" + str(int((x % 3600) / 60))
else:
minute = "" + str(int(x / 60))
if int(x % 60) < 10:
second = ":0" + str(int(x % 60))
else:
second = ":" + str(int(x % 60))
return hour + minute + second
def current_music_pause():
'''
Pauses the current song playing.
'''
global game_soundtrack_pause, pausedstate
pausedstate = True
if not music.is_playing(channel='music_room'):
return
else:
soundtrack_position = music.get_pos(channel = 'music_room') + 1.6
if soundtrack_position is not None:
game_soundtrack_pause = ("<from " + str(soundtrack_position) + ">"
+ game_soundtrack.path)
music.stop(channel='music_room',fadeout=2.0)
def current_music_play():
'''
Plays either the paused state of the current song or a new song to the
player.
'''
global pausedstate
pausedstate = False
if not game_soundtrack_pause:
music.play(game_soundtrack.path, channel = 'music_room', fadein=2.0)
else:
music.play(game_soundtrack_pause, channel = 'music_room', fadein=2.0)
def current_music_forward():
'''
Fast-forwards the song by 5 seconds or advances to the next song.
'''
global game_soundtrack_pause
if music.get_pos(channel = 'music_room') is None:
soundtrack_position = time_position + 5
else:
soundtrack_position = music.get_pos(channel = 'music_room') + 5
if soundtrack_position >= time_duration:
game_soundtrack_pause = False
if randomSong:
random_song()
else:
next_track()
else:
game_soundtrack_pause = ("<from " + str(soundtrack_position) + ">"
+ game_soundtrack.path)
music.play(game_soundtrack_pause, channel = 'music_room')
def current_music_backward():
'''
Rewinds the song by 5 seconds or advances to the next song behind it.
'''
global game_soundtrack_pause
if music.get_pos(channel = 'music_room') is None:
soundtrack_position = time_position - 5
else:
soundtrack_position = music.get_pos(channel = 'music_room') - 5
if soundtrack_position <= 0.0:
game_soundtrack_pause = False
next_track(True)
else:
game_soundtrack_pause = ("<from " + str(soundtrack_position) + ">"
+ game_soundtrack.path)
music.play(game_soundtrack_pause, channel = 'music_room')
def next_track(back=False):
'''
Advances to the next song ahead or behind to the player or the start/end.
'''
global game_soundtrack
for index, item in enumerate(soundtracks):
if (game_soundtrack.description == item.description
and game_soundtrack.name == item.name):
try:
if back:
game_soundtrack = soundtracks[index-1]
else:
game_soundtrack = soundtracks[index+1]
except:
if back:
game_soundtrack = soundtracks[-1]
else:
game_soundtrack = soundtracks[0]
break
if game_soundtrack != False:
music.play(game_soundtrack.path, channel='music_room', loop=loopSong)
def random_song():
'''
Advances to the next song with pure randomness.
'''
global game_soundtrack
unique = 1
if soundtracks[-1].path == game_soundtrack.path:
pass
else:
while unique != 0:
a = random.randrange(0, len(soundtracks)-1)
if game_soundtrack != soundtracks[a]:
unique = 0
game_soundtrack = soundtracks[a]
if game_soundtrack != False:
music.play(game_soundtrack.path, channel='music_room', loop=loopSong)
def mute_player():
'''
Mutes the music player.
'''
global old_volume
if renpy.game.preferences.get_volume("music_room_mixer") != 0.0:
old_volume = renpy.game.preferences.get_volume("music_room_mixer")
renpy.game.preferences.set_volume("music_room_mixer", 0.0)
else:
if old_volume == 0.0:
renpy.game.preferences.set_volume("music_room_mixer", 0.5)
else:
renpy.game.preferences.set_volume("music_room_mixer", old_volume)
def refresh_list():
'''
Refreshes the song list.
'''
scan_song()
if renpy.config.developer or renpy.config.developer == "auto":
rpa_mapping()
resort()
def resort():
'''
Adds songs to the song list and resorts them by priority or A-Z.
'''
global soundtracks
soundtracks = []
for obj in autoDefineList:
if obj.unlocked:
soundtracks.append(obj)
for obj in manualDefineList:
if obj.unlocked:
soundtracks.append(obj)
if organizeAZ:
soundtracks = sorted(soundtracks, key=lambda soundtracks:
soundtracks.name)
if organizePriority:
soundtracks = sorted(soundtracks, key=lambda soundtracks:
soundtracks.priority)
def get_info(path, tags):
'''
Gets the info of the tracks in the track info for defining.
'''
sec = tags.duration
try:
image_data = tags.get_image()
with open(os.path.join(gamedir, "python-packages/binaries.txt"), "rb") as a:
lines = a.readlines()
jpgbytes = bytes("\\xff\\xd8\\xff")
utfbytes = bytes("o\\x00v\\x00e\\x00r\\x00\\x00\\x00\\x89PNG\\r\\n")
jpgmatch = re.search(jpgbytes, image_data)
utfmatch = re.search(utfbytes, image_data)
if jpgmatch:
cover_formats=".jpg"
else:
cover_formats=".png"
if utfmatch: # addresses itunes cover descriptor fixes
image_data = re.sub(utfbytes, lines[2], image_data)
coverAlbum = re.sub(r"\[|\]|/|:|\?",'', tags.album)
with open(os.path.join(gamedir, 'track/covers', coverAlbum + cover_formats), 'wb') as f:
f.write(image_data)
art = coverAlbum + cover_formats
return tags.title, tags.artist, sec, art, tags.album, tags.comment
except TypeError:
return tags.title, tags.artist, sec, None, tags.album, tags.comment
def scan_song():
'''
Scans the track folder for songs and defines them to the player.
'''
global autoDefineList
exists = []
for x in autoDefineList[:]:
try:
renpy.exports.file(x.path)
exists.append(x.path)
except:
autoDefineList.remove(x)
for x in os.listdir(gamedir + '/track'):
if x.endswith((file_types)) and "track/" + x not in exists:
path = "track/" + x
tags = TinyTag.get(gamedir + "/" + path, image=True)
title, artist, sec, altAlbum, album, comment = get_info(path, tags)
def_song(title, artist, path, priorityScan, sec, altAlbum, album,
comment, True)
def def_song(title, artist, path, priority, sec, altAlbum, album, comment,
unlocked=True):
'''
Defines the song to the music player list.
'''
if title is None:
title = str(path.replace("track/", "")).capitalize()
if artist is None or artist == "":
artist = "Unknown Artist"
if altAlbum is None or altAlbum == "":
altAlbum = "images/music_room/nocover.png"
else:
altAlbum = "track/covers/"+altAlbum
try:
renpy.exports.image_size(altAlbum)
except:
altAlbum = "images/music_room/nocover.png"
if album is None or album == "":
description = "Non-Metadata Song"
else:
if comment is None:
description = album
else:
description = album + '\n' + comment
class_name = re.sub(r"-|'| ", "_", title)
class_name = soundtrack(
name = title,
author = artist,
path = path,
byteTime = sec,
priority = priority,
description = description,
cover_art = altAlbum,
unlocked = unlocked
)
autoDefineList.append(class_name)
def rpa_mapping():
'''
Maps songs in the track folder to a JSON for APK/RPA packing.
'''
data = []
try: os.remove(os.path.join(gamedir, "RPASongMetadata.json"))
except: pass
for y in autoDefineList:
data.append ({
"class": re.sub(r"-|'| ", "_", y.name),
"title": y.name,
"artist": y.author,
"path": y.path,
"sec": y.byteTime,
"altAlbum": y.cover_art,
"description": y.description,
"unlocked": y.unlocked,
})
with open(gamedir + "/RPASongMetadata.json", "a") as f:
json.dump(data, f)
def rpa_load_mapping():
'''
Loads the JSON mapping and defines it to the player.
'''
try: renpy.exports.file("RPASongMetadata.json")
except: return
with renpy.exports.file("RPASongMetadata.json") as f:
data = json.load(f)
for p in data:
title, artist, path, sec, altAlbum, description, unlocked = (p['title'],
p['artist'],
p["path"],
p["sec"],
p["altAlbum"],
p["description"],
p["unlocked"])
p['class'] = soundtrack(
name = title,
author = artist,
path = path,
byteTime = sec,
priority = priorityScan,
description = description,
cover_art = altAlbum,
unlocked = unlocked
)
autoDefineList.append(p['class'])
def get_music_channel_info():
'''
Gets the info of the music channel for exiting purposes.
'''
global prevTrack
prevTrack = music.get_playing(channel='music')
if prevTrack is None:
prevTrack = False
def check_paused_state():
'''
Checks if the music player is in a paused state for exiting purposes.
'''
if not game_soundtrack or pausedstate:
return
else:
current_music_pause()
try: os.mkdir(gamedir + "/track")
except: pass
try: os.mkdir(gamedir + "/track/covers")
except: pass
for x in os.listdir(gamedir + '/track/covers'):
os.remove(gamedir + '/track/covers/' + x)
scan_song()
if renpy.config.developer or renpy.config.developer == "auto":
rpa_mapping()
else:
rpa_load_mapping()
resort()