Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multistream support #89

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions sp/src/game/server/hl2/hl2_player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -437,29 +437,33 @@ CON_COMMAND(chaos_test_effect, "turn on a specific effect")
}
CON_COMMAND(chaos_vote_internal_poll, "used by an external client. returns vote number and possible choices for this vote")
{
ConMsg("%d;%s;%s;%s;%s",
ConMsg("%d;%s:%d;%s:%d;%s:%d;%s:%d",
g_iVoteNumber,
STRING(g_ChaosEffects[g_arriVoteEffects[0]]->m_strHudName),
g_arriVotes[0],
STRING(g_ChaosEffects[g_arriVoteEffects[1]]->m_strHudName),
g_arriVotes[1],
STRING(g_ChaosEffects[g_arriVoteEffects[2]]->m_strHudName),
STRING(g_ChaosEffects[g_arriVoteEffects[3]]->m_strHudName)
g_arriVotes[2],
STRING(g_ChaosEffects[g_arriVoteEffects[3]]->m_strHudName),
g_arriVotes[3]
);

}
CON_COMMAND(chaos_vote_internal_set, "used by an external client. sets current votes")
CON_COMMAND(chaos_vote_internal_update, "used by an external client. updates current votes")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason i was setting instead of updating votes is so we don't have to maintain two separate states. I think doing it this way is error prone, leads to worse readability, and increases the amount of rcon calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You right, but what is alternatives? Set youtube and twitch votes in two separate arrays and then sum them when choosing effect? Or you can recommend something better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually wanted to mention this option, but completely forgot when writing lol. Something like a map of name:votes and you'd call chaos_vote_internal_set youtube 42 1 2 3 4

{
// TODO: assumes vote number and 4 choices.
if (args.ArgC() < 6)
return;

int givenVoteNumber = atoi(args[1]);

if (givenVoteNumber != g_iVoteNumber)
return;

for (int i = 0; i < 4; ++i)
{
g_arriVotes[i] = atoi(args[i+2]);

// New vote
if (args.ArgC() >= 3) {
g_arriVotes[atoi(args[2])] = g_arriVotes[atoi(args[2])] + 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory corruption bug here. g_arriVotes is only 4 values long

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that when was testing, but forgot 💀

}
// Old vote
if (args.ArgC() == 4) {
g_arriVotes[atoi(args[3])] = g_arriVotes[atoi(args[3])] - 1;
}
}
CON_COMMAND(chaos_vote_debug, "prints info about the votes")
Expand Down
2 changes: 1 addition & 1 deletion twitch-integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
twitchAPI==3.11.0
rcon==2.4.2
rcon==2.4.6
61 changes: 32 additions & 29 deletions twitch-integration/twitch_integration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
import threading
from collections import Counter
import traceback
from typing import Optional

from aiohttp.helpers import sentinel
Expand All @@ -9,7 +9,7 @@
# noinspection PyUnresolvedReferences
import obspython as obs
from rcon.source import rcon
from rcon.exceptions import WrongPassword
from rcon.exceptions import WrongPassword, EmptyResponse

TARGET_CHANNEL = ''
SOURCE_NAME = ''
Expand All @@ -18,9 +18,8 @@
RCON_PASSWORD = ""

voteNumber = -1
# Counter(votes.values())
votes = {}
voteEffects = []
voteOptions = []
voteKeywords = []


Expand Down Expand Up @@ -59,39 +58,40 @@ async def on_ready(ready_event: EventData):
if TARGET_CHANNEL:
await ready_event.chat.join_room(TARGET_CHANNEL)

async def update_game_votes(newVote, oldVote = None):
global voteNumber
vote_params = [str(newVote)]
if oldVote is not None:
vote_params.append(str(oldVote))
await rcon(
'chaos_vote_internal_update', str(voteNumber), *vote_params,
host=RCON_HOST, port=int(RCON_PORT), passwd=RCON_PASSWORD, enforce_id=False
)

async def on_message(msg: ChatMessage):
global votes
print(f'in {msg.room.name}, {msg.user.name} said: {msg.text}')
uppercase_keywords = [kw.upper() for kw in voteKeywords]
if msg.text.upper() in uppercase_keywords:
kwIndex = uppercase_keywords.index(msg.text.upper())
oldVote = None
if msg.user.id in votes:
oldVote = votes[msg.user.id]
#check range of index because we don't want a 2 when the range in OBS is 4-7
if voteNumber % 2 == 0:
if kwIndex >= 4:
votes[msg.user.id] = kwIndex - 4
else:
if kwIndex <= 3:
votes[msg.user.id] = kwIndex


async def update_game_votes():
global voteNumber, voteEffects, votes
vote_counts = Counter(votes.values())
vote_params = [""] * len(voteEffects)
for i in range(len(voteEffects)):
vote_params[i] = str(vote_counts.get(i) or 0)
await rcon(
'chaos_vote_internal_set', str(voteNumber), *vote_params,
host=RCON_HOST, port=int(RCON_PORT), passwd=RCON_PASSWORD
)

if msg.user.id in votes:
await update_game_votes(votes[msg.user.id], oldVote)

async def poll_game():
global voteNumber, voteEffects, votes
global voteNumber, voteOptions, votes
raw_resp = await rcon(
'chaos_vote_internal_poll',
host=RCON_HOST, port=int(RCON_PORT), passwd=RCON_PASSWORD
host=RCON_HOST, port=int(RCON_PORT), passwd=RCON_PASSWORD, enforce_id=False
)
response = raw_resp.split("rcon from \"", 1)[0].strip()
if response == "":
Expand All @@ -100,9 +100,9 @@ async def poll_game():
vote_number, *effects = response.split(";")
vote_number = int(vote_number)

voteOptions = effects
if vote_number != voteNumber:
voteNumber = vote_number
voteEffects = effects
votes = {}

return True
Expand All @@ -118,8 +118,7 @@ async def game_loop():
else:
faulty_password = ""
try:
if await poll_game():
await update_game_votes()
await poll_game()
if already_printed_err:
print("poll resumed as normal.")
already_printed_err = False
Expand All @@ -130,9 +129,14 @@ async def game_loop():
except WrongPassword:
print("rcon wrong password")
faulty_password = RCON_PASSWORD
except EmptyResponse:
pass
except Exception as e:
print("poll unexpected exception:", e) # i broke something. log and bail
return
if not already_printed_err:
print(
"poll unexpected exception:", e
) # i broke something. log and bail
already_printed_err = True


chat: Optional[Chat] = None
Expand Down Expand Up @@ -194,14 +198,13 @@ def update_source():
if SOURCE_NAME == "":
return
output = f"Vote #{voteNumber}\n"
vote_counts = Counter(votes.values())
offset = 0
for i, voteEffect in enumerate(voteEffects):
for i, voteEffect in enumerate(voteOptions):
if i == 0 and voteNumber % 2 == 0:
offset = 4
keyword = '?' if i > len(voteKeywords) - 1 else voteKeywords[i + offset]
vote_count = vote_counts.get(i) or 0
output = output + f"{keyword} {voteEffect}: {vote_count}\n"
name, amount = voteEffect.split(":")
keyword = "?" if i > len(voteKeywords) - 1 else voteKeywords[i + offset]
output = output + f"{keyword} {name}: {amount}\n"
if i == 3 and voteNumber % 2 == 1:
break
set_text(SOURCE_NAME, output[:-1]) # exclude final newline
Expand Down
2 changes: 1 addition & 1 deletion youtube-integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pytchat
rcon==2.4.4
rcon==2.4.6
6 changes: 3 additions & 3 deletions youtube-integration/setup.bat
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ for %%A in (%appdata%\obs-studio\basic\scenes\*.json) do set /a cnt+=1
if %cnt% == 0 (
echo Couldn't find OBS scenes, you'll have to do step 10 manually.
) else if %cnt% == 1 (
py -c "import glob; import os; import json; print('You must run this script in twitch-integration folder.') or quit(0) if not os.path.exists(os.getcwd() + '\\youtube_integration.py') else None; f = open(glob.glob(os.path.expandvars('${appdata}/obs-studio/basic/scenes/*.json'))[0], 'r'); data = json.loads(f.read()); f.close(); quit(0) if any('youtube_integration.py' in script['path'] for script in data['modules']['scripts-tool']) else None; data['modules']['scripts-tool'].append({'path': os.getcwd().replace('\\', '/') + '/youtube_integration.py', 'settings': {}}); dump = json.dumps(data); f = open(glob.glob(os.path.expandvars('${appdata}/obs-studio/basic/scenes/*.json'))[0], 'w'); f.write(dump); f.close();"
py -c "import glob; import os; import json; print('You must run this script in youtube-integration folder.') or quit(0) if not os.path.exists(os.getcwd() + '\\youtube_integration.py') else None; f = open(glob.glob(os.path.expandvars('${appdata}/obs-studio/basic/scenes/*.json'))[0], 'r'); data = json.loads(f.read()); f.close(); quit(0) if any('youtube_integration.py' in script['path'] for script in data['modules']['scripts-tool']) else None; data['modules']['scripts-tool'].append({'path': os.getcwd().replace('\\', '/') + '/youtube_integration.py', 'settings': {}}); dump = json.dumps(data); f = open(glob.glob(os.path.expandvars('${appdata}/obs-studio/basic/scenes/*.json'))[0], 'w'); f.write(dump); f.close();"
echo Step 10 done.
) else (
py -c "import glob; import os; import json; print('You must run this script in twitch-integration folder.') or quit(0) if not os.path.exists(os.getcwd() + '\\youtube_integration.py') else None; user_input = input('In which profile to add twitch integration? Possible answer [' + ', '.join(path.split('\\')[-1].split('.')[0].lower() for path in glob.glob(os.path.expandvars('${appdata}/obs-studio/basic/scenes/*.json'))) + '] - '); print('Incorrect value. If you want to try again, run the script again.') or quit(0) if not os.path.exists(os.path.expandvars('${appdata}/obs-studio/basic/scenes/'+ user_input + '.json')) else None; f = open(os.path.expandvars('${appdata}/obs-studio/basic/scenes/'+ user_input + '.json'), 'r'); data = json.loads(f.read()); f.close(); quit(0) if any('youtube_integration.py' in script['path'] for script in data['modules']['scripts-tool']) else None; data['modules']['scripts-tool'].append({'path': os.getcwd().replace('\\', '/') + '/youtube_integration.py', 'settings': {}}); dump = json.dumps(data); f = open(os.path.expandvars('${appdata}/obs-studio/basic/scenes/'+ user_input + '.json'), 'w'); f.write(dump); f.close();"
py -c "import glob; import os; import json; print('You must run this script in youtube-integration folder.') or quit(0) if not os.path.exists(os.getcwd() + '\\youtube_integration.py') else None; user_input = input('In which profile to add youtube integration? Possible answer [' + ', '.join(path.split('\\')[-1].split('.')[0].lower() for path in glob.glob(os.path.expandvars('${appdata}/obs-studio/basic/scenes/*.json'))) + '] - '); print('Incorrect value. If you want to try again, run the script again.') or quit(0) if not os.path.exists(os.path.expandvars('${appdata}/obs-studio/basic/scenes/'+ user_input + '.json')) else None; f = open(os.path.expandvars('${appdata}/obs-studio/basic/scenes/'+ user_input + '.json'), 'r'); data = json.loads(f.read()); f.close(); quit(0) if any('youtube_integration.py' in script['path'] for script in data['modules']['scripts-tool']) else None; data['modules']['scripts-tool'].append({'path': os.getcwd().replace('\\', '/') + '/youtube_integration.py', 'settings': {}}); dump = json.dumps(data); f = open(os.path.expandvars('${appdata}/obs-studio/basic/scenes/'+ user_input + '.json'), 'w'); f.write(dump); f.close();"
echo Step 10 done.
)

echo Done.
pause
pause
51 changes: 27 additions & 24 deletions youtube-integration/youtube_integration.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import threading
from collections import Counter
import traceback
from typing import Optional
import time
from queue import Queue, Empty

from pytchat.core import PytchatCore
from rcon.source import Client
from rcon.exceptions import WrongPassword
from rcon.exceptions import WrongPassword, EmptyResponse

# noinspection PyUnresolvedReferences
import obspython as obs
Expand All @@ -25,39 +25,38 @@
RCON_PASSWORD = ""

voteNumber = -1
# Counter(votes.values())
votes = {}
voteEffects = []
voteOptions = []
voteKeywords = []


def update_game_votes(rcon):
global voteNumber, voteEffects, votes
vote_counts = Counter(votes.values())
vote_params = [""] * len(voteEffects)
for i in range(len(voteEffects)):
vote_params[i] = str(vote_counts.get(i) or 0)
rcon.run("chaos_vote_internal_set", str(voteNumber), *vote_params)
def update_game_votes(newVote, oldVote = None):
global voteNumber, voteOptions, votes
vote_params = [str(newVote)]
if oldVote is not None:
vote_params.append(str(oldVote))
print(rcon.run("chaos_vote_internal_update", str(voteNumber), *vote_params, enforce_id=False))


def poll_game(rcon):
global voteNumber, voteEffects, votes
raw_resp = rcon.run("chaos_vote_internal_poll")
def poll_game():
global voteNumber, voteOptions, votes
raw_resp = rcon.run("chaos_vote_internal_poll", enforce_id=False)
response = raw_resp.split('rcon from "', 1)[0].strip()
if response == "":
return False # the game is not quite ready yet.

vote_number, *effects = response.split(";")
vote_number = int(vote_number)

voteOptions = effects
if vote_number != voteNumber:
voteNumber = vote_number
voteEffects = effects
votes = {}
return True


def game_loop():
global rcon
STARTUP.wait()
already_printed_err = False
faulty_password = ""
Expand All @@ -75,8 +74,7 @@ def game_loop():
if rcon is None:
rcon = Client(RCON_HOST, int(RCON_PORT), passwd=RCON_PASSWORD)
rcon.connect(True)
if poll_game(rcon):
update_game_votes(rcon)
poll_game()
if already_printed_err:
print("poll resumed as normal.")
already_printed_err = False
Expand All @@ -89,22 +87,24 @@ def game_loop():
print("rcon wrong password")
faulty_password = RCON_PASSWORD
rcon = None
except EmptyResponse:
pass
except Exception as e:
# traceback.print_exception(e)
if not already_printed_err:
print(
"poll unexpected exception:", e
) # i broke something. log and bail
already_printed_err = True
rcon = None
# return



STARTUP = threading.Event()
CAN_CONNECT = threading.Event()
CAN_CONNECT.set()
SHUTDOWN = False
chat: Optional[PytchatCore] = None
rcon: Client = None
game_thread: Optional[threading.Thread] = None
chat_thread: Optional[threading.Thread] = None

Expand Down Expand Up @@ -172,14 +172,18 @@ def chat_loop():
uppercase_keywords = [kw.upper() for kw in voteKeywords]
if text in uppercase_keywords:
kwIndex = uppercase_keywords.index(text)
oldVote = None
if message.author.channelId in votes:
oldVote = votes[message.author.channelId]
#check range of index because we don't want a 2 when the range in OBS is 4-7
if voteNumber % 2 == 0:
if kwIndex >= 4:
votes[message.author.channelId] = kwIndex - 4
else:
if kwIndex <= 3:
votes[message.author.channelId] = kwIndex

if message.author.channelId in votes:
update_game_votes(votes[message.author.channelId], oldVote)
time.sleep(1.5)

except Exception as e:
Expand Down Expand Up @@ -208,14 +212,13 @@ def update_source():
if SOURCE_NAME == "":
return
output = f"Vote #{voteNumber}\n"
vote_counts = Counter(votes.values())
offset = 0
for i, voteEffect in enumerate(voteEffects):
for i, voteEffect in enumerate(voteOptions):
if i == 0 and voteNumber % 2 == 0:
offset = 4
name, amount = voteEffect.split(":")
keyword = "?" if i > len(voteKeywords) - 1 else voteKeywords[i + offset]
vote_count = vote_counts.get(i) or 0
output = output + f"{keyword} {voteEffect}: {vote_count}\n"
output = output + f"{keyword} {name}: {amount}\n"
if i == 3 and voteNumber % 2 == 1:
break
set_text(SOURCE_NAME, output[:-1]) # exclude final newline
Expand Down