ArabDesert/Assets/harry_t/us/ht8b.cs

4579 lines
140 KiB
C#
Raw Permalink Normal View History

2024-05-25 09:10:35 +03:00
/*
https://www.harrygodden.com
HT8B v1.8.0
Update log:
16.12.2020 (0.1.3a) - Fix for new game, wrong local turn colour
- Fix for not losing match when scratch on pot 8 ball ( Thanks: Photographotter, Mystical )
- Added permission info to console when fail reset
17.12.2020 (0.2.0a) - Predictive physics for cue ball
- Fix for not winning when sink 8, and objective on same turn ( Thanks: Rosy )
- Reduced code spaghet in decode routine
- Improved algorithm for post-game state checking, should lend
to easier implementation of optional rules.
- Allow colour switching between UK/USA/Default colour sets
- Grips change colour based on which turn it is
0.3.0a - Added desktop mode
- Sink opponents ball = loss turn
- Removed coloursets
0.3.1a - Desktop QOL
0.3.2a - Reduced sensitivity
- Added pad bytes
0.3.7a - Quest support
0.3.8a - Switched network string to base64 encoded
- Changed initial break setup
1.0.0 - First full Release
1.5.0 - Physics rework to be more accurate (Marlow implementation)
- Menu remade to be more buttony
1.5.3 - Physics patches, misc bug fixes
1.5.4 - Winner ID being set incorrectly due to menu fixed
1.5.41 - Fix for quest fail to build (thanks MrDummyNL, Legoman99573)
1.5.42 - Corrected cue colliding function for 4 ball mode
- Removed / cleaned unused preprocessor directives
- Fixed buttons that are disabled from being processed
- Fix for timer being incorrectly labelled
- Static resolution
- Unlicense everything
- Rename _frp to _log and similar
1.6.0 - Edge colliders use veroni regions and minkowski difference
to detect and apply collisions
- Removed atan2 usage in mobile shaders
- Upgraded pocket triggers to be circles
- Fixed issue with desktop mode breaking due to script order
- Added dynamic cubemap that refreshes on newgame
- Updated table geometry
1.6.2 - Fix for error on regions Ma, Mc for z(B) > z(A) causing warp ( Thanks: Traediras )
1.6.3 - Removed check that would prevent error recovery
18.03.2021 1.7.0a - Use config string for collision data / colours
- Misc bugs (improved in-editor ux)
Networking Model Information:
When a turn ends, the player who is currently playing will pack information into the
networking string that the turn has been transferred, and once the remote client who is
associated with the opposite cue recieves the update, they will take ownership of the main
script.
The local player will have a 'permit' to shoot when it is their turn, which allows them
to interact with the physics world. As soon as the cue ball is shot, the script calculates
and compresses the necessery velocities and positions of the balls, and 1. sends that out
to remote clients, and 2. decodes it the same way themselves. So effectively all players
end up watching the exact same simulation at very close to the same time. In testing this
was immediate as it could be with a GB -> USA connection.
Information about the data:
- Data is transfered using 1 Udon Synced string which is 82 bytes long, encoded to base64( 110 bytes )
- Critical game states are packed into a bitmask at #19
- Floating point positions are encoded/decoded as follows:
Encode:
Divide the value by the expected maximum range
Multiply that by signed short max value ~32k
Add signed short max
Cast to ushort
Decode:
Cast ushort to float
Subtract short max
Divide by short max
Multiply by the same range encoded with
- Ball ID's are designed around bitmasks and are as follows:
byte | Byte 0 | Byte 1 |
bit | x80 . x40 . x20 . x10 . x08 . x04 . x02 | x1 .. x80 . x40 . x20 . x10 . x08 . x04 | x02 | x01 |
ball | 15 14 13 12 11 10 9 | 7 6 5 4 3 2 1 | 8 | cue |
Networking Layout:
Total size: 78 bytes over network // 39 C# wchar
Address What Data type
[ 0x00 ] ball positions (compressed quantized vec2's)
[ 0x40 ] cue ball velocity ^
[ 0x44 ] cue ball angular vel ^
[ 0x4A ] sn_pocketed uint16 bitmask ( above table )
OR sn_gmspec | bit # | mask | what |
| 0-3 | 0x0f | fb_scores[ 0 ] |
| 4-7 | 0xf0 | fb_scores[ 1 ] |
[ 0x4C ] game state flags | bit # | mask | what |
| 0 | 0x1 | sn_simulating |
| 1 | 0x2 | sn_turnid |
| 2 | 0x4 | sn_foul |
| 3 | 0x8 | sn_open |
| 4 | 0x10 | sn_playerxor |
| 5 | 0x20 | sn_gameover |
| 6 | 0x40 | sn_winnerid |
| 7 | 0x80 | sn_permit |
| 8-10 | 0x700 | sn_gamemode |
| 11 | 0x800 | sn_lobbyopen |
| 12 | 0x1000 | <reserved> |
| 13-14 | 0x6000 | sn_timer |
| 15 | 0x8000 | sn_allowteams |
[ 0x4E ] packet # uint16
[ 0x50 ] gameid uint16
Physics Implementation:
The implementation is designed to be numerically stable (eg. using linear algebra as
much as possible to be explicit about what and where stuff collides ).
Ball physic response is 100% pure elastic energy transfer, which even at one iteration
per physics update seems to give plausable enough results.
Edge collisions are a little contrived and the reason why the table can ONLY be placed
at world orign. the table is divided into major and minor sections. some of the
calculations can be peeked at here: https://www.geogebra.org/m/jcteyvj6 . It is all
straight line equations.
There MAY be deviations between SOME client cpus / platforms depending on the floating
point architecture, and who knows what the fuck C# will decide to do at runtime anyway.
However after some testing this seems rare enough that we could not observe any
differences at all. If it does happen to be calculated differently, the remote clients
will catch up with the players game anyway. I reckon this is most likely going to
affect, if it does at all, only quest/pc crossplay and not much else.
Physics are calculated on a fixed timestep, using accumulator model. If there is very
low framerate physics may run at a slower timescale if it passes the threshold where
maximum updates/frame is reached, but won't affect eventual outcome.
Limited CCD is used on the cueball only, I do not know how to implement a variable timestep
to account for every ball, especially on slow execution time (udon)
live: wrld_d02883d1-7d79-4d51-87e2-b318ca4c2b37
dev: wrld_9497c2da-97ee-4b2e-9f82-f9adb024b6fe
*/
// https://feedback.vrchat.com/feature-requests/p/udon-expose-shaderpropertytoid
// #define USE_INT_UNIFORMS
#if !UNITY_ANDROID
//#define HT8B_DEBUGGER
#else
#define HT_QUEST
#endif
using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;
using System;
[DefaultExecutionOrder(129)]
public class ht8b : UdonSharpBehaviour
{
#region R_CONSTANTS
#if HT_QUEST
const float k_MAX_DELTA = 0.075f; // Maximum steps/frame ( 5 ish )
#else
const float k_MAX_DELTA = 0.1f; // Maximum steps/frame ( 8 )
#endif
// Physics calculation constants (measurements are in meters)
// CONFIGURATION VALUES
[SerializeField] [HideInInspector] public float k_TABLE_WIDTH; // horizontal span of table (v1.6: 1.054)
[SerializeField] [HideInInspector] public float k_TABLE_HEIGHT; // vertical span of table (v1.6: 0.605)
[SerializeField] [HideInInspector] public float k_CUSHION_RADIUS; // The roundess of colliders (v1.6: 0.043)
[SerializeField] [HideInInspector] public float k_POCKET_RADIUS; // Full diameter of pockets (v1.6: 0.100)
[SerializeField] [HideInInspector] public float k_INNER_RADIUS; // Pocket 'hitbox' cylinder (v1.6: 0.072)
// Global
[SerializeField] [HideInInspector] public Color k_colour_foul; // v1.6: ( 1.2, 0.0, 0.0, 1.0 )
[SerializeField] [HideInInspector] public Color k_colour_default; // v1.6: ( 1.0, 1.0, 1.0, 1.0 )
Color k_colour_off = new Color(0.01f, 0.01f, 0.01f, 1.0f);
// 8 ball teams
[SerializeField] [HideInInspector] public Color k_teamColour_spots; // v1.6: ( 0.00, 0.75, 1.75, 1.0 )
[SerializeField] [HideInInspector] public Color k_teamColour_stripes; // v1.6: ( 1.75, 0.25, 0.00, 1.0 )
// 4 Ball teams
[SerializeField] [HideInInspector] public Color k_colour4Ball_team_0; // v1.6: ( )
[SerializeField] [HideInInspector] public Color k_colour4Ball_team_1; // v1.6: ( 2.0, 1.0, 0.0, 1.0 )
// Fabrics colours
[SerializeField] [HideInInspector] public Color k_fabricColour_8ball; // v1.6: ( 0.3, 0.3, 0.3, 1.0 )
[SerializeField] [HideInInspector] public Color k_fabricColour_9ball; // v1.6: ( 0.1, 0.6, 1.0, 1.0 )
[SerializeField] [HideInInspector] public Color k_fabricColour_4ball; // v1.6: ( 0.15, 0.75, 0.3, 1.0 )
// pocket colliders
[SerializeField] [HideInInspector] public Vector3 k_vE; // v1.6: ( 1.087, 0.0, 0.627 )
[SerializeField] [HideInInspector] public Vector3 k_vF; // v1.6: ( 0.000, 0.0, 0.665 )
// Materials
[SerializeField] [HideInInspector] public Material ballMaterial;
[SerializeField] public Material tableMaterial;
[SerializeField] [HideInInspector] public Texture[] textureSets;
const float k_FIXED_TIME_STEP = 0.0125f; // time step in seconds per iteration
const float k_FIXED_SUBSTEP = 0.00125f;
const float k_TIME_ALPHA = 50.0f; // (unused) physics interpolation
const float k_BALL_DIAMETRE = 0.06f; // width of ball
const float k_BALL_PL_X = 0.03f; // break placement X
const float k_BALL_PL_Y = 0.05196152422f; // Break placement Y
const float k_BALL_1OR = 33.3333333333f; // 1 over ball radius
const float k_BALL_RSQR = 0.0009f; // ball radius squared
const float k_BALL_DSQR = 0.0036f; // ball diameter squared
const float k_BALL_DSQRPE = 0.003598f; // ball diameter squared plus epsilon
const float k_CUSHION_RSTT = 0.79f; // Coefficient of restituion against cushion
const float k_PI_2 = 1.57079632679f; // Pi over 2
const float k_BALL_RADIUS = 0.03f;
float k_MINOR_REGION_CONST;
const float k_1OR2 = 0.70710678118f; // 1 over root 2 (normalize +-1,+-1 vector)
const float k_1OR5 = 0.4472135955f; // 1 over root 5 (normalize +-1,+-2 vector)
const float k_RANDOMIZE_F = 0.0001f;
const float k_POCKET_DEPTH = 0.04f; // How far back (roughly) do pockets absorb balls after this point
const float k_MIN_VELOCITY = 0.00005625f; // SQUARED
const float k_F_SLIDE = 0.2f; // Friction coefficient of sliding
const float k_F_ROLL = 0.01f; // Friction coefficient of rolling
const float k_SPOT_POSITION_X = 0.5334f; // First X position of the racked balls
const float k_SPOT_CAROM_X = 0.8001f; // Spot position for carom mode
const float k_RACK_HEIGHT = -0.0764f; // Rack position on Y axis
const float k_GRAVITY = 9.80665f; // Earths gravitational acceleration
const float k_BALL_MASS = 0.16f; // Weight of ball in kg
Vector3 k_CONTACT_POINT = new Vector3(0.0f, -0.03f, 0.0f);
#if HT_QUEST
uint ANDROID_UNIFORM_CLOCK = 0x00u;
uint ANDROID_CLOCK_DIVIDER = 0x8u;
#endif
// Colour constants
Color k_markerColourOK = new Color(0.0f, 1.0f, 0.0f, 1.0f);
Color k_markerColourNO = new Color(1.0f, 0.0f, 0.0f, 1.0f);
Color k_gripColourActive = new Color(0.0f, 0.5f, 1.1f, 1.0f);
Color k_gripColourInactive = new Color(0.34f, 0.34f, 0.34f, 1.0f);
Color k_aimColour_aim = new Color(0.7f, 0.7f, 0.7f, 1.0f);
Color k_aimColour_locked = new Color(1.0f, 1.0f, 1.0f, 1.0f);
const string LOG_LOW = "<color=\"#ADADAD\">";
const string LOG_ERR = "<color=\"#B84139\">";
const string LOG_WARN = "<color=\"#DEC521\">";
const string LOG_YES = "<color=\"#69D128\">";
const string LOG_END = "</color>";
#endregion
#region R_INSPECTOR
[Space(10)]
[Header("Materials")]
[SerializeField] Material guidelineMat;
[SerializeField] Material[] CueGripMaterials;
[SerializeField] Material markerMaterial;
[SerializeField] Material timerMaterial;
// Textures
[Header("Textures")]
[SerializeField] Texture scorecard8ball;
[SerializeField] Texture scorecard4ball;
[SerializeField] Texture scorecard9ball;
[Space(10)]
[Header("Sound Effects")]
[SerializeField] AudioClip snd_Intro;
[SerializeField] AudioClip snd_Sink;
[SerializeField] AudioClip[] snd_Hits;
[SerializeField] AudioClip snd_NewTurn;
[SerializeField] AudioClip snd_PointMade;
//[SerializeField] AudioClip snd_bad;
[SerializeField] AudioClip snd_btn;
[SerializeField] AudioClip snd_spin;
[SerializeField] AudioClip snd_spinstop;
[SerializeField] AudioClip snd_hitball;
[Space(10)]
[Header("Internal (no touching!)")]
// Other scripts
[SerializeField] ht8b_cue[] gripControllers;
// GameObjects
[SerializeField] public GameObject[] balls_render;
[SerializeField] public GameObject cuetip;
[SerializeField] GameObject guideline;
[SerializeField] GameObject guidefspin;
[SerializeField] GameObject devhit;
[SerializeField] GameObject[] playerTotems;
[SerializeField] GameObject[] cueTips;
[SerializeField] GameObject infBaseTransform;
[SerializeField] GameObject markerObj;
[SerializeField] GameObject marker9ball;
[SerializeField] GameObject tableoverlayUI;
[SerializeField] GameObject m_base;
[SerializeField] GameObject point4ball;
[SerializeField] GameObject[] cueRenderObjs;
[SerializeField] GameObject select4b;
[SerializeField] GameObject[] timers;
// 1.8 + (config autos)
GameObject auto_rackPosition;
GameObject auto_pocketblockers;
GameObject auto_colliderBaseVFX;
// Meshes
//[SerializeField] Mesh[] cueball_meshes;
//[SerializeField] Mesh _9ball_mesh;
[SerializeField] Mesh _4ball_mesh_add;
[SerializeField] Mesh _4ball_mesh_minus;
// Texts
[SerializeField] Text ltext;
[SerializeField] Text[] playerNames;
[SerializeField] Text infText;
[SerializeField] Text infReset;
// Renderers
[SerializeField] Material scoreCardMat;
[SerializeField] ReflectionProbe reflection_main;
[SerializeField] VRC.Udon.UdonBehaviour bhv_cfgData;
#endregion
// Audio Components
AudioSource aud_main;
#region R_GAMESTATE
[UdonSynced] private string netstr; // dumpster fire
private string netstr_prv;
byte[] net_data = new byte[0x52];
// Networked gamestate
// data positions are marked as <#ushort>:<#bit> (<hexmask>) <description>
uint sn_pocketed = 0x00U; // 18:0 (0xffff) Each bit represents each ball, if it has been pocketed or not
bool sn_simulating = false; // 19:0 (0x01) True whilst balls are rolling
uint sn_turnid = 0x00U; // 19:1 (0x02) Whos turn is it, 0 or 1
bool sn_foul = false; // 19:2 (0x04) End-of-turn foul marker
bool sn_open = true; // 19:3 (0x08) Is the table open?
uint sn_playerxor = 0x00; // 19:4 (0x10) What colour the players have chosen
bool sn_gameover = true; // 19:5 (0x20) Game is complete
uint sn_winnerid = 0x00U; // 19:6 (0x40) Who won the game if sn_gameover is set
bool sn_lobbyclosed = true;
[HideInInspector]
public bool sn_permit = false; // 19:7 (0x80) Permission for player to play
uint sn_gamemode = 0; // 19:8 (0x700) Gamemode ID 3 bit { 0: 8 ball, 1: 9 ball, 2+: undefined }
uint sn_timer = 0; // 19:13 (0x6000) Timer ID 2 bit { 0: inf, 1: 60s, 2: 30s, 3: undefined }
bool sn_teams = false; // 19:15 (0x8000) Teams on/off (1 bit)
ushort sn_packetid = 0; // 20:0 (0xffff) Current packet number, used for locking updates so we dont accidently go back.
// this behaviour was observed on some long connections so its necessary
ushort sn_gameid = 0; // 21:0 (0xffff) Game number
ushort sn_gmspec = 0; // 22:0 (0xffff) Game mode specific information
// Cannot making a struct in C#, therefore values are duplicated
// for gamestate deltas
uint sn_pocketed_prv;
uint sn_turnid_prv;
bool sn_open_prv;
bool sn_gameover_prv;
ushort sn_gameid_prv;
uint sn_gamemode_prv;
uint sn_timer_prv;
bool sn_teams_prv;
bool sn_lobbyclosed_prv;
// Local gamestates
[HideInInspector]
public bool sn_armed = false; // Player is hitting
bool sn_updatelock = false; // We are waiting for our local simulation to finish, before we unpack data
int sn_firsthit = 0; // The first ball to be hit by cue ball
int sn_secondhit = 0;
int sn_thirdhit = 0;
bool sn_oursim = false; // If the simulation was initiated by us, only set from update
byte sn_wins0 = 0; // Wins for player 0 (unused)
byte sn_wins1 = 0; // Wins for player 1 (unused)
float introAminTimer = 0.0f; // Ball dropper timer
bool ballsMoving = false; // Tracker variable to see if balls are still on the go
bool isReposition = false; // Repositioner is active
float repoMaxX; // For clamping to table or set lower for kitchen
float timer_end = 0.0f; // What should the timer run out at
float timer_recip = 0.0f; // 1 over time limit
bool timer_running = false;
bool particle_alive = false;
float particle_time = 0.0f;
bool fb_madepoint = false;
bool fb_madefoul = false;
bool fb_jp = false;
bool fb_kr = false;
int[] fb_scores = new int[2];
bool gm_is_0 = false;
bool gm_is_1 = false;
bool gm_is_2 = false;
//bool gm_is_3 = false;
bool gm_practice = false; // Game should run in practice mode
bool region_selected = false;
bool dk_shootui = false;
// Overide / standard mesh packs
Mesh[] meshes_balls_override = new Mesh[4]; // Currently used for 4 ball mode
Mesh[] meshes_balls_regular = new Mesh[4];
// Values that will get sucked in from the menu
[HideInInspector] public int local_playerid = -1;
uint local_teamid = 0u; // Interpreted value
VRCPlayerApi[] start_saved_players = new VRCPlayerApi[2];
#endregion
#region R_PHYS_MEM
Vector3[] ball_CO = new Vector3[16]; // Current positions
Vector3[] ball_V = new Vector3[16]; // Current velocities
Vector3[] ball_W = new Vector3[16]; // Angular velocities
#endregion
// Shader uniforms
// *udon currently does not support integer uniform identifiers
#if USE_INT_UNIFORMS
int uniform_tablecolour;
int uniform_scorecard_colour0;
int uniform_scorecard_colour1;
int uniform_scorecard_info;
int uniform_marker_colour;
int uniform_cue_colour;
#else
const string uniform_tablecolour = "_EmissionColor";
const string uniform_clothcolour = "_Color";
const string uniform_scorecard_colour0 = "_Colour0";
const string uniform_scorecard_colour1 = "_Colour1";
const string uniform_scorecard_info = "_Info";
const string uniform_marker_colour = "_Color";
const string uniform_cue_colour = "_ReColor";
public name_gen _name_gen;
#endif
#region R_VFX
Color tableSrcColour = new Color(1.0f, 1.0f, 1.0f, 1.0f); // Runtime target colour
Color tableCurrentColour = new Color(1.0f, 1.0f, 1.0f, 1.0f); // Runtime actual colour
// 'Pointer' colours.
Color pColour0; // Team 0
Color pColour1; // Team 1
Color pColour2; // No team / open / 9 ball
Color pColourErr;
Color pClothColour;
// Updates table colour target to appropriate player colour
void _vis_apply_tablecolour(uint idsrc)
{
if (gm_is_2)
{
if (sn_turnid == 0)
{
cueRenderObjs[0].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, pColour0);
cueRenderObjs[1].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, pColour1 * 0.333f);
}
else
{
cueRenderObjs[0].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, pColour0 * 0.333f);
cueRenderObjs[1].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, pColour1);
}
if ((idsrc ^ sn_playerxor) == 0)
{
// Set table colour to blue
tableSrcColour = pColour0;
}
else
{
// Table colour to orange
tableSrcColour = pColour1;
}
}
else if (gm_is_1)
{
cueRenderObjs[sn_turnid].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, k_colour_default);
cueRenderObjs[sn_turnid ^ 0x1u].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, k_colour_off);
tableSrcColour = pColour2;
}
else
{
if (!sn_open)
{
if ((idsrc ^ sn_playerxor) == 0)
{
// Set table colour to blue
tableSrcColour = pColour0;
}
else
{
// Table colour to orange
tableSrcColour = pColour1;
}
cueRenderObjs[sn_playerxor].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, pColour0);
cueRenderObjs[sn_playerxor ^ 0x1u].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, pColour1);
}
else
{
tableSrcColour = pColour2;
cueRenderObjs[sn_turnid].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, k_colour_default);
cueRenderObjs[sn_turnid ^ 0x1u].GetComponent<MeshRenderer>().sharedMaterial.SetColor(uniform_cue_colour, k_colour_off);
}
}
CueGripMaterials[sn_turnid].SetColor(uniform_marker_colour, k_gripColourActive);
CueGripMaterials[sn_turnid ^ 0x1u].SetColor(uniform_marker_colour, k_gripColourInactive);
}
void _vis_hidetimers()
{
timers[0].SetActive(false);
timers[1].SetActive(false);
}
void _vis_showballs()
{
if (gm_is_1)
{
for (int i = 0; i <= 9; i++)
balls_render[i].SetActive(true);
for (int i = 10; i < 16; i++)
balls_render[i].SetActive(false);
}
else if (gm_is_2)
{
for (int i = 1; i < 16; i++)
balls_render[i].SetActive(false);
balls_render[0].SetActive(true);
balls_render[13].SetActive(true);
balls_render[14].SetActive(true);
balls_render[15].SetActive(true);
}
else
{
for (int i = 0; i < 16; i++)
{
balls_render[i].SetActive(true);
}
}
}
public void _vis_updatecoloursources()
{
if (gm_is_1) // 9 Ball / USA colours
{
pColour0 = k_colour_default;
pColour1 = k_colour_default;
pColour2 = k_colour_default;
pColourErr = k_colour_default; // No error effect
pClothColour = k_fabricColour_9ball;
// 9 ball only uses one colourset / cloth colour
ballMaterial.SetTexture("_MainTex", textureSets[1]);
}
else if (gm_is_2)
{
pColour0 = k_colour4Ball_team_0;
pColour1 = k_colour4Ball_team_1;
// Should not be used
pColour2 = k_colour_foul;
pColourErr = k_colour_foul;
ballMaterial.SetTexture("_MainTex", textureSets[1]);
pClothColour = k_fabricColour_4ball;
}
else // Standard 8 ball derivatives
{
pColourErr = k_colour_foul;
pColour2 = k_colour_default;
pColour0 = k_teamColour_spots;
pColour1 = k_teamColour_stripes;
ballMaterial.SetTexture("_MainTex", textureSets[0]);
pClothColour = k_fabricColour_8ball;
}
tableMaterial.SetColor(uniform_clothcolour, pClothColour);
}
void _vis_disableobjects()
{
guideline.SetActive(false);
devhit.SetActive(false);
infBaseTransform.SetActive(false);
markerObj.SetActive(false);
tableoverlayUI.SetActive(false);
marker9ball.SetActive(false);
point4ball.SetActive(false);
select4b.SetActive(false);
_vis_hidetimers();
}
void _vis_spawn_floaty(Vector3 pos, Mesh m)
{
point4ball.SetActive(true);
particle_alive = true;
particle_time = 0.1f;
// orient to be looking at player
Vector3 lpos = Networking.LocalPlayer.GetPosition();
Vector3 delta = lpos - transform_Surface.TransformPoint(pos);
float r = Mathf.Atan2(delta.x, delta.z);
point4ball.transform.localRotation = Quaternion.AngleAxis(r * Mathf.Rad2Deg, Vector3.up);
// set position
point4ball.transform.localPosition = pos;
// Set scale
point4ball.transform.localScale = Vector3.zero;
point4ball.GetComponent<MeshFilter>().sharedMesh = m;
}
void _vis_floaty_eval()
{
float scale, s, v, e;
// Evaluate time
particle_time += Time.deltaTime * 0.25f;
// Sustained step
s = Mathf.Max(particle_time - 0.1f, 0.0f);
v = Mathf.Min(particle_time * particle_time * 100.0f, 21.0f * s * Mathf.Exp(-15.0f * s));
// Exponential step
e = Mathf.Exp(-17.0f * Mathf.Pow(Mathf.Max(particle_time - 1.2f, 0.0f), 3.0f));
scale = e * v * 2.0f;
// Set scale
point4ball.transform.localScale = new Vector3(scale, scale, scale);
// Set position
Vector3 temp = point4ball.transform.localPosition;
temp.y = particle_time * 0.5f;
point4ball.transform.localPosition = temp;
// Particle death
if (particle_time > 2.0f)
{
particle_alive = false;
point4ball.SetActive(false);
}
}
#endregion
void _timer_reset()
{
if (sn_timer == 2) // 30s
{
timer_end = Time.timeSinceLevelLoad + 30.0f;
timer_recip = 0.03333333333f;
}
else // 60s
{
timer_end = Time.timeSinceLevelLoad + 60.0f;
timer_recip = 0.01666666666f;
}
timers[0].SetActive(true);
timers[1].SetActive(true);
timer_running = true;
}
#region R_LOCALEV
void _onlocal_carompoint(Vector3 p)
{
fb_madepoint = true;
aud_main.PlayOneShot(snd_PointMade, 1.0f);
fb_scores[sn_turnid]++;
if (fb_scores[sn_turnid] > 10)
{
fb_scores[sn_turnid] = 10;
}
_vis_spawn_floaty(p, _4ball_mesh_add);
}
void _onlocal_carompenalize(Vector3 p)
{
fb_madefoul = true;
//aud_main.PlayOneShot( snd_bad, 1.0f );
fb_scores[sn_turnid]--;
if (fb_scores[sn_turnid] < 0)
{
fb_scores[sn_turnid] = 0;
}
_vis_spawn_floaty(p, _4ball_mesh_minus);
}
// Called when a player first sinks a ball whilst the table was previously open
void _onlocal_tableclosed()
{
uint picker = sn_turnid ^ sn_playerxor;
#if HT8B_DEBUGGER
_log( LOG_YES + "(local) " + Networking.GetOwner( playerTotems[ sn_turnid ] ).displayName + ":" + sn_turnid + " is " +
(picker == 0? "blues": "oranges") + LOG_END );
#endif
_vis_apply_tablecolour(sn_turnid);
_onlocal_updatescorecard();
scoreCardMat.SetColor(uniform_scorecard_colour0, sn_playerxor == 0 ? pColour0 : pColour1);
scoreCardMat.SetColor(uniform_scorecard_colour1, sn_playerxor == 1 ? pColour0 : pColour1);
}
// End of the game. Both with/loss
void _onlocal_gameover()
{
_vis_apply_tablecolour(sn_winnerid);
#if HT8B_DEBUGGER
_log( LOG_WARN + sn_packetid + " >> local: " + local_playerid + " Winner: " + sn_winnerid + LOG_END );
#endif
if (start_saved_players[sn_winnerid] != null)
{
#if HT8B_DEBUGGER
_log( LOG_YES + "(local) Winner of match: " + start_saved_players[ sn_winnerid ].displayName + LOG_END );
#endif
infText.text = start_saved_players[sn_winnerid].displayName + " wins!";
}
else
{
infText.text = "[Unknown player] wins!";
}
infBaseTransform.SetActive(true);
marker9ball.SetActive(false);
tableoverlayUI.SetActive(false);
#if !HT_QUEST
_vis_rackballs(); // To make sure rigidbodies are completely off
#endif
_vis_hidetimers();
isReposition = false;
markerObj.SetActive(false);
_onlocal_updatescorecard();
// Remove any access rights
local_playerid = -1;
_apply_cue_access();
_htmenu_enter();
}
void _onlocal_turnchange()
{
// Effects
_vis_apply_tablecolour(sn_turnid);
aud_main.PlayOneShot(snd_NewTurn, 1.0f);
// Register correct cuetip
cuetip = cueTips[sn_turnid];
bool isOurTurn = ((local_playerid >= 0) && (local_teamid == sn_turnid)) || gm_practice;
if (gm_is_2) // 4 ball
{
// Swap cue ball and opponent cue ball
Vector3 temp = ball_CO[0];
ball_CO[0] = ball_CO[13];
ball_CO[13] = temp;
// Swap visual meshes
if (sn_turnid == 0)
{
balls_render[0].GetComponent<MeshFilter>().sharedMesh = meshes_balls_override[0];
balls_render[13].GetComponent<MeshFilter>().sharedMesh = meshes_balls_override[1];
}
else
{
balls_render[13].GetComponent<MeshFilter>().sharedMesh = meshes_balls_override[0];
balls_render[0].GetComponent<MeshFilter>().sharedMesh = meshes_balls_override[1];
}
}
else
{
// White was pocketed
if ((sn_pocketed & 0x1u) == 0x1u)
{
ball_CO[0] = Vector3.zero;
ball_V[0] = Vector3.zero;
ball_W[0] = Vector3.zero;
sn_pocketed &= 0xFFFFFFFEu;
}
}
if (isOurTurn)
{
if (sn_foul)
{
isReposition = true;
repoMaxX = k_pR.x;
markerObj.SetActive(true);
markerObj.transform.localPosition = ball_CO[0];
}
}
// Force timer reset
if (sn_timer > 0)
{
_timer_reset();
}
}
void _onlocal_updatescorecard()
{
if (gm_is_2)
{
scoreCardMat.SetVector(uniform_scorecard_info, new Vector4(fb_scores[0] * 0.04681905f, fb_scores[1] * 0.04681905f, 0.0f, 0.0f));
}
else
{
int[] counter0 = new int[2];
uint temp = sn_pocketed;
for (int j = 0; j < 2; j++)
{
for (int i = 0; i < 7; i++)
{
if ((temp & 0x4) > 0)
{
counter0[j ^ sn_playerxor]++;
}
temp >>= 1;
}
}
// Add black ball if we are winning the thing
if (sn_gameover)
{
counter0[sn_winnerid] += (int)((sn_pocketed & 0x2) >> 1);
}
scoreCardMat.SetVector(uniform_scorecard_info, new Vector4(counter0[0] * 0.0625f, counter0[1] * 0.0625f, 0.0f, 0.0f));
}
}
// Player scored an objective ball
void _onlocal_pocket_obj()
{
// Make a bright flash
tableCurrentColour *= 1.9f;
aud_main.PlayOneShot(snd_Sink, 1.0f);
}
// Player scored a foul ball (cue, non-objective or 8 before set cleared)
void _onlocal_pocket_enm()
{
tableCurrentColour = pColourErr;
aud_main.PlayOneShot(snd_Sink, 1.0f);
}
// once balls stops rolling this is called
void _onlocal_sim_end()
{
sn_simulating = false;
#if HT8B_DEBUGGER
_log( LOG_LOW + "(local) SimEnd()" + LOG_END );
#endif
// Make sure we only run this from the client who initiated the move
if (sn_oursim)
{
sn_oursim = false;
// We are updating the game state so make sure we are network owner
Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
// Owner state checks
#if HT8B_DEBUGGER
_log( LOG_LOW + "Post-move state checking" + LOG_END );
#endif
uint bmask = 0xFFFCu;
uint emask = 0x0u;
// Quash down the mask if table has closed
if (!sn_open)
{
bmask = bmask & (0x1FCu << ((int)(sn_playerxor ^ sn_turnid) * 7));
emask = 0x1FCu << ((int)(sn_playerxor ^ sn_turnid ^ 0x1U) * 7);
}
// Common informations
bool isSetComplete = (sn_pocketed & bmask) == bmask;
bool isScratch = (sn_pocketed & 0x1U) == 0x1U;
// Append black to mask if set is done
if (isSetComplete)
{
bmask |= 0x2U;
}
// These are the resultant states we can set for each mode
// then the rest is taken care of
bool
isObjectiveSink,
isOpponentSink,
winCondition,
foulCondition,
deferLossCondition
;
if (gm_is_0) // Standard 8 ball
{
isObjectiveSink = (sn_pocketed & bmask) > (sn_pocketed_prv & bmask);
isOpponentSink = (sn_pocketed & emask) > (sn_pocketed_prv & emask);
// Calculate if objective was not hit first
bool isWrongHit = ((0x1U << sn_firsthit) & bmask) == 0;
bool is8Sink = (sn_pocketed & 0x2U) == 0x2U;
winCondition = isSetComplete && is8Sink;
foulCondition = isScratch || isWrongHit;
deferLossCondition = is8Sink;
}
else if (gm_is_1) // 9 ball
{
// Rules are from: https://www.youtube.com/watch?v=U0SbHOXCtFw
// Rule #1: Cueball must strike the lowest number ball, first
bool isWrongHit = !(_lowest_ball(sn_pocketed_prv) == sn_firsthit);
// Rule #2: Pocketing cueball, is a foul
// Win condition: Pocket 9 ball ( at anytime )
winCondition = (sn_pocketed & 0x200u) == 0x200u;
// this video is hard to follow so im just gonna guess this is right
isObjectiveSink = (sn_pocketed & 0x3FEu) > (sn_pocketed_prv & 0x3FEu);
isOpponentSink = false;
deferLossCondition = false;
foulCondition = isWrongHit || isScratch;
// TODO: Implement rail contact requirement
}
else if (gm_is_2) // 4 ball
{
isObjectiveSink = fb_madepoint;
isOpponentSink = fb_madefoul;
foulCondition = false;
deferLossCondition = false;
winCondition = fb_scores[sn_turnid] >= 10;
}
else // Unkown mode
{
isObjectiveSink = true;
isOpponentSink = false;
winCondition = false;
foulCondition = false;
deferLossCondition = false;
if ((sn_pocketed & 0x1u) == 0x1u)
{
sn_foul = true;
_onlocal_turnchange();
}
}
if (winCondition)
{
if (foulCondition)
{
// Loss
_turn_win(sn_turnid ^ 0x1U);
}
else
{
// Win
_turn_win(sn_turnid);
}
}
else if (deferLossCondition)
{
// Loss
_turn_win(sn_turnid ^ 0x1U);
}
else if (foulCondition)
{
// Foul
_turn_foul();
}
else if (isObjectiveSink && !isOpponentSink)
{
// Continue
_turn_continue();
}
else
{
// Pass
_turn_pass();
}
}
// Check if there was a network update on hold
if (sn_updatelock)
{
#if HT8B_DEBUGGER
_log( LOG_LOW + "Update was waiting, executing now" + LOG_END );
#endif
sn_updatelock = false;
_netread();
}
}
void _onlocal_timer_end()
{
timer_running = false;
#if HT8B_DEBUGGER
_log( LOG_ERR + "Out of time!!" + LOG_END );
#endif
// We are holding the stick so propogate the change
if (Networking.GetOwner(playerTotems[sn_turnid]) == Networking.LocalPlayer)
{
Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
_turn_foul();
}
else
{
// All local players freeze until next target
// can pick up and propogate timer end
sn_permit = false;
}
_vis_hidetimers();
}
// Grant cue access if we are playing
void _apply_cue_access()
{
if (local_playerid >= 0)
{
if (gm_practice)
{
gripControllers[0]._access_allow();
gripControllers[1]._access_allow();
}
else
{
if ((local_teamid & 0x1) > 0) // Local player is 1, or 3
{
gripControllers[1]._access_allow();
gripControllers[0]._access_deny();
}
else // Local player is 0, or 2
{
gripControllers[0]._access_allow();
gripControllers[1]._access_deny();
}
}
}
else
{
gripControllers[0]._access_deny();
gripControllers[1]._access_deny();
}
}
// Some udon specific optimisations
void _setup_gm_spec()
{
gm_is_0 = sn_gamemode == 0u;
gm_is_1 = sn_gamemode == 1u;
gm_is_2 = sn_gamemode == 2u;
}
void _onlocal_newgame()
{
#if HT8B_DEBUGGER
_log( LOG_LOW + "NewGameLocal()" + LOG_END );
#endif
// Take a copy of player apis
start_saved_players[0] = Networking.GetOwner(playerTotems[0]);
start_saved_players[1] = Networking.GetOwner(playerTotems[1]);
_setup_gm_spec();
// Calculate interpreted values from menu states
if (local_playerid >= 0)
local_teamid = (uint)local_playerid & 0x1u;
// Disable menu
_htmenu_exit();
// Reflect menu-state settings (for late joiners)
_vis_updatecoloursources();
_vis_apply_tablecolour(0);
_apply_cue_access();
if (gm_is_2) // 4 ball specific
{
auto_pocketblockers.SetActive(true);
scoreCardMat.SetTexture("_MainTex", scorecard4ball);
scoreCardMat.SetColor(uniform_scorecard_colour0, pColour0);
scoreCardMat.SetColor(uniform_scorecard_colour1, pColour1);
fb_scores[0] = 0;
fb_scores[1] = 0;
// Set mesh filters on balls that change them
balls_render[0].GetComponent<MeshFilter>().sharedMesh = meshes_balls_override[0];
balls_render[13].GetComponent<MeshFilter>().sharedMesh = meshes_balls_override[1];
balls_render[14].GetComponent<MeshFilter>().sharedMesh = meshes_balls_override[2];
balls_render[15].GetComponent<MeshFilter>().sharedMesh = meshes_balls_override[3];
}
else
{
auto_pocketblockers.SetActive(false);
scoreCardMat.SetTexture("_MainTex", scorecard8ball);
// Reset mesh filters on balls that change them
balls_render[0].GetComponent<MeshFilter>().sharedMesh = meshes_balls_regular[0];
balls_render[13].GetComponent<MeshFilter>().sharedMesh = meshes_balls_regular[1];
balls_render[14].GetComponent<MeshFilter>().sharedMesh = meshes_balls_regular[2];
balls_render[15].GetComponent<MeshFilter>().sharedMesh = meshes_balls_regular[3];
}
if (gm_is_1) // 9 ball specific
{
scoreCardMat.SetTexture("_MainTex", scorecard9ball);
marker9ball.SetActive(true);
}
else
{
marker9ball.SetActive(false);
}
_vis_showballs();
// Reflect game state
_onlocal_updatescorecard();
isReposition = false;
markerObj.SetActive(false);
infBaseTransform.SetActive(false);
// Effects
introAminTimer = 2.0f;
aud_main.PlayOneShot(snd_Intro, 1.0f);
// Player name texts
string base_text = "";
if (sn_teams)
{
base_text = "Team ";
}
tableoverlayUI.SetActive(true);
playerNames[0].text = base_text + Networking.GetOwner(playerTotems[0]).displayName;
playerNames[1].text = base_text + Networking.GetOwner(playerTotems[1]).displayName;
timer_running = false;
// Switch desktop/vr
bool usr_desktop = !Networking.LocalPlayer.IsUserInVR();
#if !HT_QUEST
gripControllers[0].useDesktop = usr_desktop;
gripControllers[1].useDesktop = usr_desktop;
#endif
reflection_main.RenderProbe();
}
#endregion
#region R_PHYS
// Cue input tracking
Vector3 cue_lpos;
Vector3 cue_llpos;
Vector3 cue_vdir;
Vector3 cue_shotdir;
float cue_fdir;
#if HT_QUEST
#else
[HideInInspector]
public Vector3 dkTargetPos; // Target for desktop aiming
#endif
// Finalize positions onto their rack spots
void _vis_rackballs()
{
uint ball_bit = 0x1u;
for (int i = 0; i < 16; i++)
{
balls_render[i].GetComponent<Rigidbody>().isKinematic = true;
if ((ball_bit & sn_pocketed) == ball_bit)
{
// Recover Y position since its lost in networking
Vector3 rack_position = ball_CO[i];
rack_position.y = k_rack_position.y;
balls_render[i].transform.localPosition = rack_position;
}
ball_bit <<= 1;
}
}
// Internal game state pocket and enable unity physics to play out the rest
void _onlocal_pocketball(int id)
{
uint total = 0U;
// Get total for X positioning
int count_extent = gm_is_1 ? 10 : 16;
for (int i = 1; i < count_extent; i++)
{
total += (sn_pocketed >> i) & 0x1U;
}
// place ball on the rack
ball_CO[id] = k_rack_position + (float)total * k_BALL_DIAMETRE * k_rack_direction;
sn_pocketed ^= 1U << id;
uint bmask = 0x1FCU << ((int)(sn_turnid ^ sn_playerxor) * 7);
// Good pocket
if (((0x1U << id) & ((bmask) | (sn_open ? 0xFFFCU : 0x0000U) | ((bmask & sn_pocketed) == bmask ? 0x2U : 0x0U))) > 0)
{
_onlocal_pocket_obj();
}
else
{
// bad
_onlocal_pocket_enm();
}
#if !HT_QUEST
// VFX ( make ball move )
Rigidbody body = balls_render[id].GetComponent<Rigidbody>();
body.isKinematic = false;
body.velocity = this.transform.TransformVector(new Vector3(
ball_V[id].x,
0.0f,
ball_V[id].z
));
#else
balls_render[ id ].transform.localPosition = ball_CO[ id ];
#endif
}
// Is cue touching another ball?
bool _cue_contacting()
{
if (gm_is_0) // 8 ball
{
// Check all
for (int i = 1; i < 16; i++)
{
if ((ball_CO[0] - ball_CO[i]).sqrMagnitude < k_BALL_DSQR)
{
return true;
}
}
}
else if (gm_is_1) // 9
{
// Only check to 9 ball
for (int i = 1; i <= 9; i++)
{
if ((ball_CO[0] - ball_CO[i]).sqrMagnitude < k_BALL_DSQR)
{
return true;
}
}
}
else // 4
{
if ((ball_CO[0] - ball_CO[9]).sqrMagnitude < k_BALL_DSQR)
{
return true;
}
if ((ball_CO[0] - ball_CO[2]).sqrMagnitude < k_BALL_DSQR)
{
return true;
}
if ((ball_CO[0] - ball_CO[3]).sqrMagnitude < k_BALL_DSQR)
{
return true;
}
}
return false;
}
const float k_SINA = 0.28078832987f;
const float k_SINA2 = 0.07884208619f;
const float k_COSA = 0.95976971915f;
const float k_COSA2 = 0.92115791379f;
const float k_EP1 = 1.79f;
const float k_A = 21.875f;
const float k_B = 6.25f;
const float k_F = 1.72909790282f;
// Apply cushion bounce
void _phy_bounce_cushion(int id, Vector3 N)
{
// Mathematical expressions derived from: https://billiards.colostate.edu/physics_articles/Mathavan_IMechE_2010.pdf
//
// (Note): subscript gamma, u, are used in replacement of Y and Z in these expressions because
// unicode does not have them.
//
// f = 2/7
// f₁ = 5/7
//
// Velocity delta:
// Δvₓ = vₓ∙( f∙sin²θ + (1+e)∙cos²θ ) Rωᵤ∙sinθ
// Δvᵧ = 0
// Δvᵤ = f₁∙vᵤ + fR( ωₓ∙sinθ - ωᵧ∙cosθ ) - vᵤ
//
// Aux:
// Sₓ = vₓ∙sinθ - vᵧ∙cosθ+ωᵤ
// Sᵧ = 0
// Sᵤ = -vᵤ - ωᵧ∙cosθ + ωₓ∙cosθ
//
// k = (5∙Sᵤ) / ( 2∙mRA )
// c = vₓ∙cosθ - vᵧ∙cosθ
//
// Angular delta:
// ωₓ = k∙sinθ
// ωᵧ = k∙cosθ
// ωᵤ = (5/(2m))∙(-Sₓ / A + ((sinθ∙c∙(e+1)) / B)∙(cosθ - sinθ))
//
// These expressions are in the reference frame of the cushion, so V and ω inputs need to be rotated
// Reject bounce if velocity is going the same way as normal
// this state means we tunneled, but it happens only on the corner
// vertexes
Vector3 source_v = ball_V[id];
if (Vector3.Dot(source_v, N) > 0.0f)
{
return;
}
// Rotate V, W to be in the reference frame of cushion
Quaternion rq = Quaternion.AngleAxis(Mathf.Atan2(-N.z, -N.x) * Mathf.Rad2Deg, Vector3.up);
Quaternion rb = Quaternion.Inverse(rq);
Vector3 V = rq * source_v;
Vector3 W = rq * ball_W[id];
Vector3 V1;
Vector3 W1;
float k, c, s_x, s_z;
//V1.x = -V.x * ((2.0f/7.0f) * k_SINA2 + k_EP1 * k_COSA2) - (2.0f/7.0f) * k_BALL_PL_X * W.z * k_SINA;
//V1.z = (5.0f/7.0f)*V.z + (2.0f/7.0f) * k_BALL_PL_X * (W.x * k_SINA - W.y * k_COSA) - V.z;
//V1.y = 0.0f;
// (baked):
V1.x = -V.x * k_F - 0.00240675711f * W.z;
V1.z = 0.71428571428f * V.z + 0.00857142857f * (W.x * k_SINA - W.y * k_COSA) - V.z;
V1.y = 0.0f;
// s_x = V.x * k_SINA - V.y * k_COSA + W.z;
// (baked): y component not used:
s_x = V.x * k_SINA + W.z;
s_z = -V.z - W.y * k_COSA + W.x * k_SINA;
// k = (5.0f * s_z) / ( 2 * k_BALL_MASS * k_A );
// (baked):
k = s_z * 0.71428571428f;
// c = V.x * k_COSA - V.y * k_COSA;
// (baked): y component not used
c = V.x * k_COSA;
W1.x = k * k_SINA;
//W1.z = (5.0f / (2.0f * k_BALL_MASS)) * (-s_x / k_A + ((k_SINA * c * k_EP1) / k_B) * (k_COSA - k_SINA));
// (baked):
W1.z = 15.625f * (-s_x * 0.04571428571f + c * 0.0546021744f);
W1.y = k * k_COSA;
// Unrotate result
ball_V[id] += rb * V1;
ball_W[id] += rb * W1;
}
Vector3 k_vA = new Vector3();
Vector3 k_vB = new Vector3();
Vector3 k_vC = new Vector3();
Vector3 k_vD = new Vector3();
Vector3 k_vX = new Vector3();
Vector3 k_vY = new Vector3();
Vector3 k_vZ = new Vector3();
Vector3 k_vW = new Vector3();
Vector3 k_pK = new Vector3();
Vector3 k_pL = new Vector3();
Vector3 k_pM = new Vector3();
Vector3 k_pN = new Vector3();
Vector3 k_pO = new Vector3();
Vector3 k_pP = new Vector3();
Vector3 k_pQ = new Vector3();
Vector3 k_pR = new Vector3();
Vector3 k_pT = new Vector3();
Vector3 k_pS = new Vector3();
Vector3 k_pU = new Vector3();
Vector3 k_pV = new Vector3();
Vector3 k_vA_vD = new Vector3();
Vector3 k_vA_vD_normal = new Vector3();
Vector3 k_vB_vY = new Vector3();
Vector3 k_vB_vY_normal = new Vector3();
Vector3 k_vC_vZ_normal = new Vector3();
Vector3 k_vA_vB_normal = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 k_vC_vW_normal = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 _sign_pos = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 k_rack_position = new Vector3();
Vector3 k_rack_direction = new Vector3();
Transform transform_Surface;
void _phy_table_init()
{
k_rack_position = transform_Surface.InverseTransformPoint(auto_rackPosition.transform.position);
k_rack_direction = transform_Surface.InverseTransformDirection(auto_rackPosition.transform.up);
// Handy values
k_MINOR_REGION_CONST = k_TABLE_WIDTH - k_TABLE_HEIGHT;
// Major source vertices
k_vA.x = k_POCKET_RADIUS * 0.92f;
k_vA.z = k_TABLE_HEIGHT;
k_vB.x = k_TABLE_WIDTH - k_POCKET_RADIUS;
k_vB.z = k_TABLE_HEIGHT;
k_vC.x = k_TABLE_WIDTH;
k_vC.z = k_TABLE_HEIGHT - k_POCKET_RADIUS;
k_vD.x = k_vA.x - 0.016f;
k_vD.z = k_vA.z + 0.060f;
// Aux points
k_vX = k_vD + Vector3.forward;
k_vW = k_vC;
k_vW.z = 0.0f;
k_vY = k_vB;
k_vY.x += 1.0f;
k_vY.z += 1.0f;
k_vZ = k_vC;
k_vZ.x += 1.0f;
k_vZ.z += 1.0f;
// Normals
k_vA_vD = k_vD - k_vA;
k_vA_vD = k_vA_vD.normalized;
k_vA_vD_normal.x = -k_vA_vD.z;
k_vA_vD_normal.z = k_vA_vD.x;
k_vB_vY = k_vB - k_vY;
k_vB_vY = k_vB_vY.normalized;
k_vB_vY_normal.x = -k_vB_vY.z;
k_vB_vY_normal.z = k_vB_vY.x;
k_vC_vZ_normal = -k_vB_vY_normal;
// Minkowski difference
k_pN = k_vA;
k_pN.z -= k_CUSHION_RADIUS;
k_pM = k_vA + k_vA_vD_normal * k_CUSHION_RADIUS;
k_pL = k_vD + k_vA_vD_normal * k_CUSHION_RADIUS;
k_pK = k_vD;
k_pK.x -= k_CUSHION_RADIUS;
k_pO = k_vB;
k_pO.z -= k_CUSHION_RADIUS;
k_pP = k_vB + k_vB_vY_normal * k_CUSHION_RADIUS;
k_pQ = k_vC + k_vC_vZ_normal * k_CUSHION_RADIUS;
k_pR = k_vC;
k_pR.x -= k_CUSHION_RADIUS;
k_pT = k_vX;
k_pT.x -= k_CUSHION_RADIUS;
k_pS = k_vW;
k_pS.x -= k_CUSHION_RADIUS;
k_pU = k_vY + k_vB_vY_normal * k_CUSHION_RADIUS;
k_pV = k_vZ + k_vC_vZ_normal * k_CUSHION_RADIUS;
k_pS = k_vW;
k_pS.x -= k_CUSHION_RADIUS;
}
// Check pocket condition
void _phy_ball_pockets(int id)
{
Vector3 A;
A = ball_CO[id];
_sign_pos.x = Mathf.Sign(A.x);
_sign_pos.z = Mathf.Sign(A.z);
A = Vector3.Scale(A, _sign_pos);
if (Vector3.Distance(A, k_vE) < k_INNER_RADIUS)
{
_onlocal_pocketball(id);
return;
}
if (Vector3.Distance(A, k_vF) < k_INNER_RADIUS)
{
_onlocal_pocketball(id);
return;
}
if (A.z > k_vF.z)
{
_onlocal_pocketball(id);
return;
}
if (A.z > -A.x + k_vE.x + k_vE.z)
{
_onlocal_pocketball(id);
return;
}
}
GameObject g_ball_current;
// Pocketless table
void _phy_ball_table_carom(int id)
{
float zz, zx;
Vector3 A;
A = ball_CO[id];
// Setup major regions
zx = Mathf.Sign(A.x);
zz = Mathf.Sign(A.z);
if (A.x * zx > k_pR.x)
{
ball_CO[id].x = k_pR.x * zx;
_phy_bounce_cushion(id, Vector3.left * zx);
}
if (A.z * zz > k_pO.z)
{
ball_CO[id].z = k_pO.z * zz;
_phy_bounce_cushion(id, Vector3.back * zz);
}
}
void _phy_ball_table_std(int id)
{
Vector3 A, N, _V, V, a_to_v;
float dot;
A = ball_CO[id];
_sign_pos.x = Mathf.Sign(A.x);
_sign_pos.z = Mathf.Sign(A.z);
A = Vector3.Scale(A, _sign_pos);
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vA, k_vB, Color.white );
Debug.DrawLine( k_vD, k_vA, Color.white );
Debug.DrawLine( k_vB, k_vY, Color.white );
Debug.DrawLine( k_vD, k_vX, Color.white );
Debug.DrawLine( k_vC, k_vW, Color.white );
Debug.DrawLine( k_vC, k_vZ, Color.white );
r_k_CUSHION_RADIUS = k_CUSHION_RADIUS-k_BALL_RADIUS;
_phy_table_init();
Debug.DrawLine( k_pT, k_pK, Color.yellow );
Debug.DrawLine( k_pK, k_pL, Color.yellow );
Debug.DrawLine( k_pL, k_pM, Color.yellow );
Debug.DrawLine( k_pM, k_pN, Color.yellow );
Debug.DrawLine( k_pN, k_pO, Color.yellow );
Debug.DrawLine( k_pO, k_pP, Color.yellow );
Debug.DrawLine( k_pP, k_pU, Color.yellow );
Debug.DrawLine( k_pV, k_pQ, Color.yellow );
Debug.DrawLine( k_pQ, k_pR, Color.yellow );
Debug.DrawLine( k_pR, k_pS, Color.yellow );
r_k_CUSHION_RADIUS = k_CUSHION_RADIUS;
_phy_table_init();
#endif
if (A.x > k_vA.x) // Major Regions
{
if (A.x > A.z + k_MINOR_REGION_CONST) // Minor B
{
if (A.z < k_TABLE_HEIGHT - k_POCKET_RADIUS)
{
// Region H
#if HT8B_DRAW_REGIONS
Debug.DrawLine( new Vector3( 0.0f, 0.0f, 0.0f ), new Vector3( k_TABLE_WIDTH, 0.0f, 0.0f ), Color.red );
Debug.DrawLine( k_vC, k_vC + k_vC_vW_normal, Color.red );
#endif
if (A.x > k_TABLE_WIDTH - k_CUSHION_RADIUS)
{
// Static resolution
A.x = k_TABLE_WIDTH - k_CUSHION_RADIUS;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(k_vC_vW_normal, _sign_pos));
}
}
else
{
a_to_v = A - k_vC;
if (Vector3.Dot(a_to_v, k_vB_vY) > 0.0f)
{
// Region I ( VORONI )
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vC, k_pR, Color.green );
Debug.DrawLine( k_vC, k_pQ, Color.green );
#endif
if (a_to_v.magnitude < k_CUSHION_RADIUS)
{
// Static resolution
N = a_to_v.normalized;
A = k_vC + N * k_CUSHION_RADIUS;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(N, _sign_pos));
}
}
else
{
// Region J
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vC, k_vB, Color.red );
Debug.DrawLine( k_pQ, k_pV, Color.blue );
#endif
a_to_v = A - k_pQ;
if (Vector3.Dot(k_vC_vZ_normal, a_to_v) < 0.0f)
{
// Static resolution
dot = Vector3.Dot(a_to_v, k_vB_vY);
A = k_pQ + dot * k_vB_vY;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(k_vC_vZ_normal, _sign_pos));
}
}
}
}
else // Minor A
{
if (A.x < k_vB.x)
{
// Region A
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vA, k_vA + k_vA_vB_normal, Color.red );
Debug.DrawLine( k_vB, k_vB + k_vA_vB_normal, Color.red );
#endif
if (A.z > k_pN.z)
{
// Velocity based A->C delegation ( scuffed CCD )
a_to_v = A - k_vA;
_V = Vector3.Scale(ball_V[id], _sign_pos);
V.x = -_V.z;
V.y = 0.0f;
V.z = _V.x;
if (A.z > k_vA.z)
{
if (Vector3.Dot(V, a_to_v) > 0.0f)
{
// Region C ( Delegated )
a_to_v = A - k_pL;
// Static resolution
dot = Vector3.Dot(a_to_v, k_vA_vD);
A = k_pL + dot * k_vA_vD;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(k_vA_vD_normal, _sign_pos));
}
else
{
// Static resolution
A.z = k_pN.z;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(k_vA_vB_normal, _sign_pos));
}
}
else
{
// Static resolution
A.z = k_pN.z;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(k_vA_vB_normal, _sign_pos));
}
}
}
else
{
a_to_v = A - k_vB;
if (Vector3.Dot(a_to_v, k_vB_vY) > 0.0f)
{
// Region F ( VERONI )
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vB, k_pO, Color.green );
Debug.DrawLine( k_vB, k_pP, Color.green );
#endif
if (a_to_v.magnitude < k_CUSHION_RADIUS)
{
// Static resolution
N = a_to_v.normalized;
A = k_vB + N * k_CUSHION_RADIUS;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(N, _sign_pos));
}
}
else
{
// Region G
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vB, k_vC, Color.red );
Debug.DrawLine( k_pP, k_pU, Color.blue );
#endif
a_to_v = A - k_pP;
if (Vector3.Dot(k_vB_vY_normal, a_to_v) < 0.0f)
{
// Static resolution
dot = Vector3.Dot(a_to_v, k_vB_vY);
A = k_pP + dot * k_vB_vY;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(k_vB_vY_normal, _sign_pos));
}
}
}
}
}
else
{
a_to_v = A - k_vA;
if (Vector3.Dot(a_to_v, k_vA_vD) > 0.0f)
{
a_to_v = A - k_vD;
if (Vector3.Dot(a_to_v, k_vA_vD) > 0.0f)
{
if (A.z > k_pK.z)
{
// Region E
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vD, k_vD + k_vC_vW_normal, Color.red );
#endif
if (A.x > k_pK.x)
{
// Static resolution
A.x = k_pK.x;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(k_vC_vW_normal, _sign_pos));
}
}
else
{
// Region D ( VORONI )
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vD, k_vD + k_vC_vW_normal, Color.green );
Debug.DrawLine( k_vD, k_vD + k_vA_vD_normal, Color.green );
#endif
if (a_to_v.magnitude < k_CUSHION_RADIUS)
{
// Static resolution
N = a_to_v.normalized;
A = k_vD + N * k_CUSHION_RADIUS;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(N, _sign_pos));
}
}
}
else
{
// Region C
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vA, k_vA + k_vA_vD_normal, Color.red );
Debug.DrawLine( k_vD, k_vD + k_vA_vD_normal, Color.red );
Debug.DrawLine( k_pL, k_pM, Color.blue );
#endif
a_to_v = A - k_pL;
if (Vector3.Dot(k_vA_vD_normal, a_to_v) < 0.0f)
{
// Static resolution
dot = Vector3.Dot(a_to_v, k_vA_vD);
A = k_pL + dot * k_vA_vD;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(k_vA_vD_normal, _sign_pos));
}
}
}
else
{
// Region B ( VORONI )
#if HT8B_DRAW_REGIONS
Debug.DrawLine( k_vA, k_vA + k_vA_vB_normal, Color.green );
Debug.DrawLine( k_vA, k_vA + k_vA_vD_normal, Color.green );
#endif
if (a_to_v.magnitude < k_CUSHION_RADIUS)
{
// Static resolution
N = a_to_v.normalized;
A = k_vA + N * k_CUSHION_RADIUS;
// Dynamic
_phy_bounce_cushion(id, Vector3.Scale(N, _sign_pos));
}
}
}
ball_CO[id] = Vector3.Scale(A, _sign_pos);
}
// Advance simulation 1 step for ball id
void _phy_ball_step(int id)
{
g_ball_current = balls_render[id];
// Since v1.5.0
Vector3 V = ball_V[id];
Vector3 W = ball_W[id];
Vector3 cv;
// Equations derived from: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.89.4627&rep=rep1&type=pdf
//
// R: Contact location with ball and floor aka: (0,-r,0)
// µₛ: Slipping friction coefficient
// µᵣ: Rolling friction coefficient
// i: Up vector aka: (0,1,0)
// g: Planet Earth's gravitation acceleration ( 9.80665 )
//
// Relative contact velocity (marlow):
// c = v + R✕ω
//
// Ball is classified as 'rolling' or 'slipping'. Rolling is when the relative velocity is none and the ball is
// said to be in pure rolling motion
//
// When ball is classified as rolling:
// Δv = -µᵣ∙g∙Δt∙(v/|v|)
//
// Angular momentum can therefore be derived as:
// ωₓ = -vᵤ/R
// ωᵧ = 0
// ωᵤ = vₓ/R
//
// In the slipping state:
// Δω = ((-5∙µₛ∙g)/(2/R))∙Δt∙i✕(c/|c|)
// Δv = -µₛ∙g∙Δt(c/|c|)
// Relative contact velocity of ball and table
cv = V + Vector3.Cross(k_CONTACT_POINT, W);
// Rolling is achieved when cv's length is approaching 0
// The epsilon is quite high here because of the fairly large timestep we are working with
if (cv.magnitude <= 0.1f)
{
//V += -k_F_ROLL * k_GRAVITY * k_FIXED_TIME_STEP * V.normalized;
// (baked):
V += -0.00122583125f * V.normalized;
// Calculate rolling angular velocity
W.x = -V.z * k_BALL_1OR;
if (0.3f > Mathf.Abs(W.y))
{
W.y = 0.0f;
}
else
{
W.y -= Mathf.Sign(W.y) * 0.3f;
}
W.z = V.x * k_BALL_1OR;
// Stopping scenario
if (V.magnitude < 0.01f && W.magnitude < 0.2f)
{
W = Vector3.zero;
V = Vector3.zero;
}
else
{
ballsMoving = true;
}
}
else // Slipping
{
Vector3 nv = cv.normalized;
// Angular slipping friction
//W += ((-5.0f * k_F_SLIDE * k_GRAVITY)/(2.0f * 0.03f)) * k_FIXED_TIME_STEP * Vector3.Cross( Vector3.up, nv );
// (baked):
W += -2.04305208f * Vector3.Cross(Vector3.up, nv);
//V += -k_F_SLIDE * k_GRAVITY * k_FIXED_TIME_STEP * nv;
// (baked):
V += -0.024516625f * nv;
ballsMoving = true;
}
ball_W[id] = W;
ball_V[id] = V;
g_ball_current.transform.Rotate(this.transform.TransformDirection(W.normalized), W.magnitude * k_FIXED_TIME_STEP * -Mathf.Rad2Deg, Space.World);
g_ball_current.GetComponent<AudioSource>().Play();
uint ball_bit = 0x1U << id;
// ball/ball collisions
for (int i = id + 1; i < 16; i++)
{
ball_bit <<= 1;
if ((ball_bit & sn_pocketed) != 0U)
continue;
Vector3 delta = ball_CO[i] - ball_CO[id];
float dist = delta.magnitude;
if (dist < k_BALL_DIAMETRE)
{
Vector3 normal = delta / dist;
// static resolution
Vector3 res = (k_BALL_DIAMETRE - dist) * normal;
ball_CO[i] += res;
ball_CO[id] -= res;
Vector3 velocityDelta = ball_V[id] - ball_V[i];
float dot = Vector3.Dot(velocityDelta, normal);
// Dynamic resolution (Cr is assumed to be (1)+1.0)
Vector3 reflection = normal * dot;
ball_V[id] -= reflection;
ball_V[i] += reflection;
// Prevent sound spam if it happens
if (ball_V[id].sqrMagnitude > 0 && ball_V[i].sqrMagnitude > 0)
{
//aud_main.PlayOneShot( snd_Hits[ 0 ], 1.0f );
g_ball_current.GetComponent<AudioSource>().PlayOneShot(snd_Hits[id % 3], Mathf.Clamp01(reflection.magnitude));
}
// First hit detection
if (id == 0)
{
if (gm_is_2)
{
if (fb_kr) // KR 사구 ( Sagu )
{
if (i == 13)
{
if (!fb_madefoul)
{
_onlocal_carompenalize(ball_CO[i]);
}
}
else
{
if (sn_firsthit == 0)
{
sn_firsthit = i;
}
else
{
if (i != sn_firsthit)
{
if (sn_secondhit == 0)
{
sn_secondhit = i;
_onlocal_carompoint(ball_CO[i]);
}
}
}
}
}
else // JP 四つ玉 ( Yotsudama )
{
if (sn_firsthit == 0)
{
sn_firsthit = i;
}
else
{
if (sn_secondhit == 0)
{
if (i != sn_firsthit)
{
sn_secondhit = i;
_onlocal_carompoint(ball_CO[i]);
}
}
else
{
if (sn_thirdhit == 0)
{
if (i != sn_firsthit && i != sn_secondhit)
{
sn_thirdhit = i;
_onlocal_carompoint(ball_CO[i]);
}
}
}
}
}
}
else
{
if (sn_firsthit == 0)
{
sn_firsthit = i;
}
}
}
}
}
}
// ( Since v0.2.0a ) Check if we can predict a collision before move update happens to improve accuracy
bool _phy_predict_cueball()
{
// Get what will be the next position
Vector3 originalDelta = ball_V[0] * k_FIXED_TIME_STEP;
Vector3 norm = ball_V[0].normalized;
Vector3 h;
float lf, s, nmag;
// Closest found values
float minlf = 9999999.0f;
int minid = 0;
float mins = 0;
uint ball_bit = 0x1U;
// Loop balls look for collisions
for (int i = 1; i < 16; i++)
{
ball_bit <<= 1;
if ((ball_bit & sn_pocketed) != 0U)
continue;
h = ball_CO[i] - ball_CO[0];
lf = Vector3.Dot(norm, h);
s = k_BALL_DSQRPE - Vector3.Dot(h, h) + lf * lf;
if (s < 0.0f)
continue;
if (lf < minlf)
{
minlf = lf;
minid = i;
mins = s;
}
}
if (minid > 0)
{
nmag = minlf - Mathf.Sqrt(mins);
// Assign new position if got appropriate magnitude
if (nmag * nmag < originalDelta.sqrMagnitude)
{
ball_CO[0] += norm * nmag;
return true;
}
}
return false;
}
// Run one physics iteration for all balls
void _phys_step()
{
ballsMoving = false;
uint ball_bit = 0x1u;
// Cue angular velocity
if ((sn_pocketed & 0x1U) == 0)
{
if (!_phy_predict_cueball())
{
// Apply movement
ball_CO[0] += ball_V[0] * k_FIXED_TIME_STEP;
}
_phy_ball_step(0);
}
// Run main simulation / inter-ball collision
for (int i = 1; i < 16; i++)
{
ball_bit <<= 1;
if ((ball_bit & sn_pocketed) == 0U)
{
ball_CO[i] += ball_V[i] * k_FIXED_TIME_STEP;
_phy_ball_step(i);
}
}
// Check if simulation has settled
if (!ballsMoving)
{
if (sn_simulating)
{
_onlocal_sim_end();
}
return;
}
if (gm_is_2)
{
_phy_ball_table_carom(0);
_phy_ball_table_carom(13);
_phy_ball_table_carom(14);
_phy_ball_table_carom(15);
}
else
{
ball_bit = 0x1U;
// Run edge collision
for (int i = 0; i < 16; i++)
{
if ((ball_bit & sn_pocketed) == 0U)
_phy_ball_table_std(i);
ball_bit <<= 1;
}
}
if (gm_is_2) return;
ball_bit = 0x1U;
// Run triggers
for (int i = 0; i < 16; i++)
{
if ((ball_bit & sn_pocketed) == 0U)
{
_phy_ball_pockets(i);
}
ball_bit <<= 1;
}
}
// Ray circle intersection
// yes, its fixed size circle
// Output is dispensed into the below variable
// One intersection point only
// This is not used in physics calcuations, only cue input
Vector2 RayCircle_output;
bool _phy_ray_circle(Vector2 start, Vector2 dir, Vector2 circle)
{
Vector2 nrm = dir.normalized;
Vector2 h = circle - start;
float lf = Vector2.Dot(nrm, h);
float s = k_BALL_RSQR - Vector2.Dot(h, h) + lf * lf;
if (s < 0.0f) return false;
s = Mathf.Sqrt(s);
if (lf < s)
{
if (lf + s >= 0)
{
s = -s;
}
else
{
return false;
}
}
RayCircle_output = start + nrm * (lf - s);
return true;
}
Vector3 RaySphere_output;
bool _phy_ray_sphere(Vector3 start, Vector3 dir, Vector3 sphere)
{
Vector3 nrm = dir.normalized;
Vector3 h = sphere - start;
float lf = Vector3.Dot(nrm, h);
float s = k_BALL_RSQR - Vector3.Dot(h, h) + lf * lf;
if (s < 0.0f) return false;
s = Mathf.Sqrt(s);
if (lf < s)
{
if (lf + s >= 0)
{
s = -s;
}
else
{
return false;
}
}
RaySphere_output = start + nrm * (lf - s);
return true;
}
// Closest point on line from pos
Vector2 _line_project(Vector2 start, Vector2 dir, Vector2 pos)
{
return start + dir * Vector2.Dot(pos - start, dir);
}
#endregion
#region R_UTIL
// Find the lowest numbered ball, that isnt the cue, on the table
// This function finds the VISUALLY represented lowest ball,
// since 8 has id 1, the search needs to be split
int _lowest_ball(uint field)
{
for (int i = 2; i <= 8; i++)
{
if (((field >> i) & 0x1U) == 0x00U)
return i;
}
if (((field) & 0x2U) == 0x00U)
return 1;
for (int i = 9; i < 16; i++)
{
if (((field >> i) & 0x1U) == 0x00U)
return i;
}
// ??
return 0;
}
#endregion
#region R_TURNRULES
void _turn_win(uint winner)
{
#if HT8B_DEBUGGER
_log( LOG_LOW + " -> GAMEOVER" + LOG_END );
#endif
sn_gameover = true;
sn_winnerid = winner;
_netpack(sn_turnid);
_netread();
_onlocal_gameover();
}
void _turn_pass()
{
#if HT8B_DEBUGGER
_log( LOG_LOW + " -> PASS" + LOG_END );
#endif
sn_permit = true;
_netpack(sn_turnid ^ 0x1u);
_netread();
}
void _turn_foul()
{
#if HT8B_DEBUGGER
_log( LOG_LOW + " -> FOUL" + LOG_END );
#endif
sn_foul = true;
sn_permit = true;
_netpack(sn_turnid ^ 0x1U);
_netread();
}
void _turn_continue()
{
#if HT8B_DEBUGGER
_log( LOG_LOW + " -> COTNINUE" + LOG_END );
#endif
// Close table if it was open ( 8 ball specific )
if (gm_is_0)
{
if (sn_open)
{
uint sink_orange = 0;
uint sink_blue = 0;
uint pmask = sn_pocketed >> 2;
for (int i = 0; i < 7; i++)
{
if ((pmask & 0x1u) == 0x1u)
sink_blue++;
pmask >>= 1;
}
for (int i = 0; i < 7; i++)
{
if ((pmask & 0x1u) == 0x1u)
sink_orange++;
pmask >>= 1;
}
if (sink_blue == sink_orange)
{
// Sunk equal amounts therefore still undecided
}
else
{
if (sink_blue > sink_orange)
{
sn_playerxor = sn_turnid;
}
else
{
sn_playerxor = sn_turnid ^ 0x1u;
}
sn_open = false;
_onlocal_tableclosed();
}
}
}
// Keep playing
sn_permit = true;
_netpack(sn_turnid);
_netread();
}
#endregion
#region R_MENU
Vector3 m_planenormal;
float m_planedist;
Vector3 m_cursor;
bool m_desktop = true;
const float k_mGmButtonW = 0.09345f;
const float k_mGmButtonH = 0.034f;
const float k_mSmolButtonR = 0.034f;
const float k_mGmButtonA = 0.01026977f; // Reset height
[Space(10)]
[Header("Menu")]
[SerializeField] GameObject[] m_gamemode_buttons;
[SerializeField] GameObject[] m_join_buttons;
//[SerializeField] GameObject m_startbutton;
[SerializeField] GameObject[] m_teambuttons;
[SerializeField] GameObject[] m_timebuttons;
bool[] m_gm_buttonstates = new bool[4];
[SerializeField] Mesh[] m_buttonmeshes;
const int k_EButtonMesh_8ball = 0;
const int k_EButtonMesh_9ball = 1;
const int k_EButtonMesh_4ball = 2;
const int k_EButtonMesh_reserved0 = 3;
const int k_EButtonMesh_green = 4;
const int k_EButtonMesh_red = 5;
const int k_EButtonMesh_blue = 6;
const int k_EButtonMesh_triangle = 7;
const int k_EButtonMesh_join_0 = 8;
const int k_EButtonMesh_join_1 = 9;
const int k_EButtonMesh_play = 10;
const uint k_ButtonState_None = 0x0u;
const uint k_ButtonState_Pressing = 0x1u;
const uint k_ButtonState_Triggered = 0x2u;
const uint k_ButtonState_ShouldReset = 0x2u;
const uint k_ButtonState_FrameMask = 0xFFFFFFFEu; // (~0x1u)
// Current check dimensions ( pulled from above )
float m_current_x = 0.0f;
float m_current_y = 0.0f;
Mesh m_current_outline;
MeshFilter m_outline_filter;
uint[] m_auto_btnstate = new uint[20];
GameObject[] m_auto_btnobjs = new GameObject[20];
int m_auto_id = -1;
[SerializeField] GameObject m_gm_dkoutline;
[SerializeField] GameObject[] m_playerslot_owners;
// VFX stuff
[SerializeField] GameObject m_TeamCover;
[SerializeField] GameObject m_TimeLimitDisp;
Vector3 m_TeamCover_target_s;
Vector3 m_TeamCover_current_s;
Vector3 m_TimeLimit_x_target;
Vector3 m_TimeLimit_x_current;
[SerializeField] GameObject m_menuLoc_main;
[SerializeField] GameObject m_menuLoc_start;
[SerializeField] GameObject m_newGameBtn;
[SerializeField] Text[] m_lobbyNames;
[SerializeField] GameObject[] rulePages;
Vector3 m_menuLoc_sw;
Vector3 m_menuLoc_swt;
VRCPlayerApi localplayer;
Vector3 _plane_line_intersect(Vector3 n, float d, Vector3 a, Vector3 b)
{
Vector3 ba = b - a;
float nDotA = Vector3.Dot(n, a);
float nDotBA = Vector3.Dot(n, ba);
return a + (((d - nDotA) / nDotBA) * ba);
}
// Setup meshes on gameobject
void _htbtn_init(GameObject button, int variant, bool state)
{
button.GetComponent<MeshFilter>().sharedMesh = m_buttonmeshes[variant * 3 + (state ? 1 : 0)];
}
void _htmenu_init()
{
// Setup button meshes
_htbtn_init(m_gamemode_buttons[0], k_EButtonMesh_8ball, sn_gamemode == 0u);
_htbtn_init(m_gamemode_buttons[1], k_EButtonMesh_9ball, sn_gamemode == 1u);
_htbtn_init(m_gamemode_buttons[2], k_EButtonMesh_4ball, sn_gamemode == 2u);
_htbtn_init(m_join_buttons[0], k_EButtonMesh_join_0, false);
_htbtn_init(m_join_buttons[1], k_EButtonMesh_join_1, false);
//_htbtn_init( m_startbutton, k_EButtonMesh_play, false );
_htbtn_init(m_teambuttons[0], k_EButtonMesh_red, !sn_teams);
_htbtn_init(m_teambuttons[1], k_EButtonMesh_green, sn_teams);
_htbtn_init(m_timebuttons[0], k_EButtonMesh_triangle, true);
_htbtn_init(m_timebuttons[1], k_EButtonMesh_triangle, true);
_htbtn_init(m_newGameBtn, k_EButtonMesh_play, true);
m_outline_filter = m_gm_dkoutline.GetComponent<MeshFilter>();
// Create surface plane
m_planenormal = m_base.transform.up;
m_planedist = Vector3.Dot(m_base.transform.position, m_planenormal);
localplayer = Networking.LocalPlayer;
m_gm_dkoutline.SetActive(false);
sn_lobbyclosed = true;
_htmenu_viewtimer();
_htmenu_viewteams();
_htmenu_viewgm();
_htmenuview();
}
// View gamemode changes
void _htmenu_viewgm()
{
for (int i = 0; i < m_gamemode_buttons.Length; i++)
{
if (sn_gamemode == (uint)i)
{
m_gamemode_buttons[i].GetComponent<MeshFilter>().sharedMesh = m_buttonmeshes[(k_EButtonMesh_8ball + i) * 3 + 1];
}
else
{
m_gamemode_buttons[i].GetComponent<MeshFilter>().sharedMesh = m_buttonmeshes[(k_EButtonMesh_8ball + i) * 3];
}
}
}
void _htmenu_viewjoin()
{
int playernum = 0;
if (!sn_lobbyclosed)
{
VRCPlayerApi host = Networking.GetOwner(m_playerslot_owners[0]);
// Check out player names
for (int i = 0; i < (sn_teams ? 4 : 2); i++)
{
VRCPlayerApi player = Networking.GetOwner(m_playerslot_owners[i]);
// Its us
if (local_playerid == i)
{
// Error: Local believes that we are in lobby, but someone else is there
if (player.playerId != Networking.LocalPlayer.playerId)
{
#if HT8B_DEBUGGER
_log( LOG_ERR + "Error: de-sync local lobby status" + LOG_END );
#endif
local_playerid = -1;
m_lobbyNames[i].text = "<color=\"#ff0000\">CONFLICT</color>";
}
else
{
playernum++;
m_lobbyNames[i].text = "<color=\"#cae4ed\">" + player.displayName + "</color>";
}
}
else
{
// Player is joined
if (host.playerId != player.playerId || i == 0)
{
m_lobbyNames[i].text = "<color=\"#ffffff\">" + player.displayName + "</color>";
playernum++;
}
else
{
m_lobbyNames[i].text = "";
}
}
}
}
gm_practice = local_playerid == 0 && playernum == 1;
// If in the game
if (local_playerid >= 0)
{
// Set our team button to the 'leave' button
_htbtn_init(m_join_buttons[local_teamid], k_EButtonMesh_join_0 + (int)local_teamid, true);
// Opposite button should become startgame/disabled, 'enabled' if player 0
if (local_playerid == 0)
{
m_join_buttons[1].SetActive(true);
_htbtn_init(m_join_buttons[1], k_EButtonMesh_play, true);
}
else
{
m_join_buttons[local_teamid ^ 0x1u].SetActive(false);
}
}
else // Otherwise, its just join buttons
{
m_join_buttons[0].SetActive(true);
m_join_buttons[1].SetActive(true);
_htbtn_init(m_join_buttons[0], k_EButtonMesh_join_0, false);
_htbtn_init(m_join_buttons[1], k_EButtonMesh_join_1, false);
}
}
uint last_viewtimer = 0u;
bool sound_spinning = false;
void _htmenu_viewtimer()
{
if (last_viewtimer != sn_timer)
{
aud_main.PlayOneShot(snd_spin);
last_viewtimer = sn_timer;
sound_spinning = true;
}
m_TimeLimit_x_target = new Vector3(-0.128f * (float)sn_timer, 0.0f, 0.0f);
}
void _htmenu_viewteams()
{
m_TeamCover_target_s = sn_teams ? new Vector3(0, 1, 1) : new Vector3(1, 1, 1);
_htbtn_init(m_teambuttons[0], k_EButtonMesh_red, !sn_teams);
_htbtn_init(m_teambuttons[1], k_EButtonMesh_green, sn_teams);
_htmenu_viewjoin();
}
void _htmenuview()
{
if (sn_lobbyclosed)
{
m_menuLoc_swt = Vector3.one;
}
else
{
m_menuLoc_swt = Vector3.zero;
}
}
uint gm_target = 0u;
float gm_minheight = Mathf.Infinity;
bool _buttonpressed(GameObject btn, int typeid)
{
// Set automatic id's
m_auto_id++;
m_auto_btnobjs[m_auto_id] = btn;
// Dont process buttons that are disabled
if (!btn.activeSelf)
{
return false;
}
Vector3 delta; Vector3 tmp_pos;
delta = btn.transform.localPosition - m_cursor;
if (Mathf.Abs(delta.x) < m_current_x && Mathf.Abs(delta.z) < m_current_y)
{
if (m_desktop)
{
// Visual transform
if (Input.GetButton("Fire1"))
{
tmp_pos = btn.transform.localPosition;
tmp_pos.y = 0.0f;
btn.transform.localPosition = tmp_pos;
}
m_gm_dkoutline.SetActive(true);
m_gm_dkoutline.transform.localPosition = btn.transform.localPosition;
m_gm_dkoutline.transform.localRotation = btn.transform.localRotation;
m_outline_filter.sharedMesh = m_buttonmeshes[typeid * 3 + 2];
// Actuation
if (Input.GetButtonDown("Fire1"))
{
_htmenu_buttonpressed();
return true;
}
}
else // VR
{
// Button range
if (m_cursor.y < k_mGmButtonA && m_cursor.y > -0.1f)
{
// Update visual position
tmp_pos = btn.transform.localPosition;
tmp_pos.y = Mathf.Clamp(m_cursor.y, 0.0f, tmp_pos.y);
btn.transform.localPosition = tmp_pos;
m_auto_btnstate[m_auto_id] |= k_ButtonState_Pressing;
if (m_cursor.y <= 0.0f) // Actuation
{
// Rising edge
if (m_auto_btnstate[m_auto_id] == k_ButtonState_Pressing)
{
m_auto_btnstate[m_auto_id] |= k_ButtonState_Triggered;
_htmenu_buttonpressed();
return true;
}
}
}
}
}
return false;
}
// Join lobby
void _htjoinplayer(int id)
{
#if HT8B_DEBUGGER
_log( LOG_YES + "_htjoinplayer: " + id + LOG_END );
#endif
local_playerid = id;
local_teamid = ((uint)id & 0x2u) >> 1;
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[id]);
_htmenu_viewjoin();
}
// Join team locally
void _htjointeam(int id)
{
#if HT8B_DEBUGGER
_log( LOG_LOW + "_htjointeam: " + id + LOG_END );
#endif
// Leave routine
if (local_playerid >= 0)
{
// Close lobby
if (local_playerid == 0)
{
if (id == 0)
{
#if HT8B_DEBUGGER
_log( LOG_ERR + "( closing lobby )" + LOG_END );
#endif
sn_lobbyclosed = true;
local_playerid = -1;
_htmenuview();
Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
_netpack_lossy();
}
else
{
#if HT8B_DEBUGGER
_log( LOG_YES + "Starting game!" + LOG_END );
#endif
region_selected = false;
_tr_newgame();
return;
}
}
else
{
if ((int)local_teamid == id)
{
#if HT8B_DEBUGGER
_log( LOG_WARN + "( leaving lobby )" + LOG_END );
#endif
// Set owner back to host
Networking.SetOwner(Networking.GetOwner(m_playerslot_owners[0]), m_playerslot_owners[local_playerid]);
// Mark locally out of game
local_playerid = -1;
}
else
{
#if HT8B_DEBUGGER
_log( LOG_LOW + "this button does nothing" + LOG_END );
#endif
}
}
_htmenu_viewjoin();
return;
}
// Create new lobby
if (sn_lobbyclosed)
{
#if HT8B_DEBUGGER
_log( LOG_YES + "Creating lobby" + LOG_END );
#endif
// Assign other players to us to signify not joined
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[1]);
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[2]);
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[3]);
sn_lobbyclosed = false;
Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
_netpack_lossy();
_htjoinplayer(0);
_htmenuview();
return;
}
VRCPlayerApi gameHost = Networking.GetOwner(m_playerslot_owners[0]);
// Check for open spot on team
// Team 1
if (id == 1)
{
if (Networking.GetOwner(m_playerslot_owners[1]).playerId == gameHost.playerId)
{
_htjoinplayer(1);
}
else if (sn_teams && (Networking.GetOwner(m_playerslot_owners[3]).playerId == gameHost.playerId))
{
_htjoinplayer(3);
}
else
{
#if HT8B_DEBUGGER
_log( LOG_ERR + "no slot availible" + LOG_END );
#endif
}
}
// Team 2
else if (sn_teams && (Networking.GetOwner(m_playerslot_owners[2]).playerId == gameHost.playerId))
{
_htjoinplayer(2);
}
else
{
#if HT8B_DEBUGGER
_log( LOG_ERR + "no slot availible" + LOG_END );
#endif
}
}
void _htmenu_resetnetwork()
{
sn_lobbyclosed = true;
if (Networking.GetOwner(this.gameObject) == Networking.LocalPlayer)
{
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[0]);
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[1]);
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[2]);
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[3]);
_netpack_lossy();
}
}
// Find button target
void _htmenu_trimin()
{
m_auto_id = -1;
GameObject btn;
// Join / Leave buttons
m_current_x = k_mGmButtonW;
m_current_y = k_mGmButtonH;
if (sn_lobbyclosed)
{
if (_buttonpressed(m_newGameBtn, k_EButtonMesh_play))
{
_htjointeam(0);
}
}
else
{
for (int i = 0; i < m_join_buttons.Length; i++)
{
btn = m_join_buttons[i];
if (_buttonpressed(btn, k_EButtonMesh_join_0 + i))
{
_htjointeam(i);
}
}
if (local_playerid == 0) // Host only
{
// Gamemode buttons
m_current_x = k_mGmButtonW;
m_current_y = k_mGmButtonH;
for (int i = 0; i < m_gamemode_buttons.Length; i++)
{
btn = m_gamemode_buttons[i];
if (_buttonpressed(btn, k_EButtonMesh_8ball + i))
{
sn_gamemode = (uint)i;
_htmenu_viewgm();
_netpack_lossy();
}
}
// Smol buttons
m_current_x = k_mSmolButtonR;
m_current_y = k_mSmolButtonR;
// Timelimit buttons
if (_buttonpressed(m_timebuttons[1], k_EButtonMesh_triangle))
{
if (sn_timer > 0)
{
sn_timer--;
_htmenu_viewtimer();
_netpack_lossy();
}
}
if (_buttonpressed(m_timebuttons[0], k_EButtonMesh_triangle))
{
if (sn_timer < 2)
{
sn_timer++;
_htmenu_viewtimer();
_netpack_lossy();
}
}
// Teams enabled buttons
if (_buttonpressed(m_teambuttons[0], k_EButtonMesh_red))
{
sn_teams = false;
// Kick players
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[2]);
Networking.SetOwner(Networking.LocalPlayer, m_playerslot_owners[3]);
_htmenu_viewteams();
_netpack_lossy();
}
if (_buttonpressed(m_teambuttons[1], k_EButtonMesh_green))
{
sn_teams = true;
_htmenu_viewteams();
_netpack_lossy();
}
}
}
}
VRC_Pickup.PickupHand _htmenu_hand = VRC_Pickup.PickupHand.None;
void _htmenu_buttonpressed()
{
aud_main.PlayOneShot(snd_btn);
if (_htmenu_hand != VRC_Pickup.PickupHand.None)
{
Networking.LocalPlayer.PlayHapticEventInHand(_htmenu_hand, 0.02f, 1.0f, 1.0f);
}
}
void _htmenu_begin()
{
Vector3 tmp_pos;
GameObject btn;
for (int i = 0; i <= m_auto_id; i++)
{
if (m_auto_btnstate[i] == k_ButtonState_ShouldReset)
{
m_auto_btnstate[i] = k_ButtonState_None;
}
// Reset button Y position
btn = m_auto_btnobjs[i];
tmp_pos = btn.transform.localPosition;
tmp_pos.y = k_mGmButtonA;
btn.transform.localPosition = tmp_pos;
// Disables pressed so it can be re-set
m_auto_btnstate[i] &= k_ButtonState_FrameMask;
}
}
void _htmenu_enter()
{
m_base.SetActive(true);
_htmenu_resetnetwork();
_htmenu_viewtimer();
_htmenu_viewteams();
_htmenu_viewgm();
_htmenu_viewjoin();
_htmenuview();
}
void _htmenu_exit()
{
sn_lobbyclosed = true;
m_base.SetActive(false);
}
float next_refresh = 0.0f;
void _htmenu_update()
{
#if UNITY_EDITOR
return;
#endif
m_desktop = !Networking.LocalPlayer.IsUserInVR();
if (Time.timeSinceLevelLoad > next_refresh)
{
_htmenu_viewjoin();
next_refresh = Time.timeSinceLevelLoad + 0.5f;
}
// Desktop: Project cursor onto plane
if (m_desktop)
{
m_gm_dkoutline.SetActive(false);
VRCPlayerApi.TrackingData hmd = localplayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
m_cursor = _plane_line_intersect(m_planenormal, m_planedist, hmd.position, hmd.position + (hmd.rotation * Vector3.forward));
// Localize m_cursor
m_cursor = m_base.transform.InverseTransformPoint(m_cursor);
_htmenu_hand = VRC_Pickup.PickupHand.None;
_htmenu_begin();
_htmenu_trimin();
}
else
{
_htmenu_begin();
// Left hand
_htmenu_hand = VRC_Pickup.PickupHand.Left;
m_cursor = m_base.transform.InverseTransformPoint(localplayer.GetBonePosition(HumanBodyBones.LeftIndexDistal));
_htmenu_trimin();
VRCPlayerApi.TrackingData leftHand = localplayer.GetTrackingData(VRCPlayerApi.TrackingDataType.LeftHand);
m_cursor = m_base.transform.InverseTransformPoint(leftHand.position);
_htmenu_trimin();
// Right hand
_htmenu_hand = VRC_Pickup.PickupHand.Right;
m_cursor = m_base.transform.InverseTransformPoint(localplayer.GetBonePosition(HumanBodyBones.RightIndexDistal));
_htmenu_trimin();
VRCPlayerApi.TrackingData rightHand = localplayer.GetTrackingData(VRCPlayerApi.TrackingDataType.RightHand);
m_cursor = m_base.transform.InverseTransformPoint(rightHand.position);
_htmenu_trimin();
}
// Update visual stuff
m_TeamCover_current_s = Vector3.Lerp(m_TeamCover_current_s, m_TeamCover_target_s, Time.deltaTime * 5.0f);
m_TimeLimit_x_current = Vector3.Lerp(m_TimeLimit_x_current, m_TimeLimit_x_target, Time.deltaTime * 5.0f);
m_menuLoc_sw = Vector3.Lerp(m_menuLoc_sw, m_menuLoc_swt, Time.deltaTime * 5.0f);
// Stop sound
if (sound_spinning && Vector3.Distance(m_TimeLimit_x_current, m_TimeLimit_x_target) < 0.01f)
{
sound_spinning = false;
aud_main.PlayOneShot(snd_spinstop);
}
m_TeamCover.transform.localScale = m_TeamCover_current_s;
m_TimeLimitDisp.transform.localPosition = m_TimeLimit_x_current;
// Menu locations scale swap
m_menuLoc_start.transform.localScale = m_menuLoc_sw;
m_menuLoc_main.transform.localScale = Vector3.one - m_menuLoc_sw;
}
#endregion
float timeLast;
float accum;
// Copy current values to previous values, no memcpy here >:(
void _sn_cpyprev()
{
// Init _prv states
sn_turnid_prv = sn_turnid;
sn_open_prv = sn_open;
sn_gameover_prv = sn_gameover;
sn_gameid_prv = sn_gameid;
// Since 1.0.0
sn_gamemode_prv = sn_gamemode;
sn_timer_prv = sn_timer;
sn_teams_prv = sn_teams;
sn_lobbyclosed_prv = sn_lobbyclosed;
//sn_pocketed_prv = sn_pocketed; this one needs to be independent
//sn_simulating_prv = sn_simulating;
//sn_foul_prv = sn_foul;
//sn_playerxor_prv = sn_playerxor;
//sn_winnerid_prv = sn_winnerid;
//sn_permit_prv = sn_permit;
}
Vector3 dkCursor = new Vector3(0.0f, 2.0f, 0.0f);
Vector3 dkHitCursor = new Vector3(0.0f, 0.0f, 0.0f);
[Space(10)]
[Header("Desktop UI")]
[SerializeField] GameObject dkCursorObj;
[SerializeField] GameObject dkHitPos;
[SerializeField] GameObject desktopBase;
[SerializeField] GameObject desktopQuad;
[SerializeField] GameObject[] dkStickBases;
[SerializeField] GameObject dkOverlayPwr;
[SerializeField] GameObject dk_E;
const float k_DesktopCursorSpeed = 0.035f;
bool dkShootingIn = false;
bool dkSafeRemove = true;
Vector3 dkShootVector;
Vector3 dkSafeRemovePoint;
float dkShootReference = 0.0f;
float dkClampX;
float dkClampY;
bool turnLocalLive = false;
bool dkFrameIgnore = false;
// Cue picked up local
public void _ht_desktop_enter()
{
dk_shootui = true;
dkFrameIgnore = true;
desktopBase.SetActive(true);
// Lock player in place
Networking.LocalPlayer.SetWalkSpeed(0.0f);
Networking.LocalPlayer.SetRunSpeed(0.0f);
Networking.LocalPlayer.SetStrafeSpeed(0.0f);
#if HT8B_DEBUGGER
_log( LOG_LOW + "Entering desktop overlay" + LOG_END );
#endif
}
// Cue put down local
public void _ht_desktop_cue_down()
{
_ht_desktopui_exit();
}
void _ht_desktopui_exit()
{
dk_shootui = false;
desktopBase.SetActive(false);
#if !HT_QUEST
gripControllers[0]._primarycontrol();
gripControllers[1]._primarycontrol();
Networking.LocalPlayer.SetWalkSpeed(2.0f);
Networking.LocalPlayer.SetRunSpeed(4.0f);
Networking.LocalPlayer.SetStrafeSpeed(2.0f);
#endif
}
void _dk_AllowHit()
{
turnLocalLive = true;
// Reset hit point
dkHitCursor = Vector3.zero;
}
void _dk_DenyHit()
{
turnLocalLive = false;
}
float shootAmt = 0.0f;
void _ht_desktopui_update()
{
if (dkFrameIgnore)
{
dkFrameIgnore = false;
return;
}
if (Input.GetKeyDown(KeyCode.E))
{
_ht_desktopui_exit();
return;
}
// Keep UI rendering
VRCPlayerApi.TrackingData hmd = localplayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
desktopQuad.transform.position = hmd.position + hmd.rotation * Vector3.forward;
dk_E.transform.position = desktopQuad.transform.position;
dkCursor.x = Mathf.Clamp
(
dkCursor.x + Input.GetAxis("Mouse X") * k_DesktopCursorSpeed,
-dkClampX,
dkClampX
);
dkCursor.z = Mathf.Clamp
(
dkCursor.z + Input.GetAxis("Mouse Y") * k_DesktopCursorSpeed,
-dkClampY,
dkClampY
);
if (turnLocalLive)
{
Vector3 ncursor = dkCursor;
ncursor.y = 0.0f;
Vector3 delta = ncursor - ball_CO[0];
GameObject cue = dkStickBases[sn_turnid];
if (Input.GetButton("Fire1"))
{
if (!dkShootingIn)
{
dkShootingIn = true;
// Create shooting vector
dkShootVector = delta.normalized;
// Project reference start point
dkShootReference = Vector3.Dot(dkShootVector, ncursor);
// Create copy of cursor for later
dkSafeRemovePoint = dkCursor;
// Unlock cursor position from table
dkClampX = Mathf.Infinity;
dkClampY = Mathf.Infinity;
}
// Calculate shoot amount via projection
shootAmt = dkShootReference - Vector3.Dot(dkShootVector, ncursor);
dkSafeRemove = shootAmt < 0.0f;
shootAmt = Mathf.Clamp(shootAmt, 0.0f, 0.5f);
// Set delta back to dkShootVector
delta = dkShootVector;
// Disable cursor in shooting mode
dkCursorObj.SetActive(false);
}
else
{
// Trigger shot
if (dkShootingIn)
{
// Shot cancel
if (dkSafeRemove)
{
}
else // FIREEEEE
{
// Fake hit ( kinda )
float vel = Mathf.Pow(shootAmt * 2.0f, 1.4f) * 9.0f;
Vector3 r_1 = (RaySphere_output - ball_CO[0]) * k_BALL_1OR;
ball_V[0] = dkShootVector * vel * Vector3.Dot(r_1, -dkShootVector);
Vector3 p = dkShootVector.normalized * vel;
ball_W[0] = Vector3.Cross(r_1, p) * -25.0f;
#if HT8B_DEBUGGER
_log( LOG_WARN + "Angular velocity: " + ball_W[ 0 ].ToString() + ". Velocity: " + ball_V[ 0 ].ToString() + LOG_END );
#endif
cue.transform.localPosition = new Vector3(2000.0f, 2000.0f, 2000.0f);
turnLocalLive = false;
_hit_general();
}
// Restore cursor position
dkCursor = dkSafeRemovePoint;
dkClampX = k_TABLE_WIDTH;
dkClampY = k_TABLE_HEIGHT;
// 1-frame override to fix rotation
delta = dkShootVector;
}
dkShootingIn = false;
shootAmt = 0.0f;
dkCursorObj.SetActive(true);
}
if (Input.GetKey(KeyCode.W))
{
dkHitCursor += Vector3.forward * Time.deltaTime;
}
if (Input.GetKey(KeyCode.S))
{
dkHitCursor += Vector3.back * Time.deltaTime;
}
if (Input.GetKey(KeyCode.A))
{
dkHitCursor += Vector3.left * Time.deltaTime;
}
if (Input.GetKey(KeyCode.D))
{
dkHitCursor += Vector3.right * Time.deltaTime;
}
// Clamp in circlee
if (dkHitCursor.magnitude > 0.90f)
{
dkHitCursor = dkHitCursor.normalized * 0.9f;
}
dkHitPos.transform.localPosition = dkHitCursor;
// Get angle
float ang = Mathf.Atan2(delta.x, delta.z);
// Create rotation
Quaternion xr = Quaternion.AngleAxis(10.0f, Vector3.right);
Quaternion r = Quaternion.AngleAxis(ang * Mathf.Rad2Deg, Vector3.up);
Vector3 worldHit = new Vector3(dkHitCursor.x * k_BALL_PL_X, dkHitCursor.z * k_BALL_PL_X, -0.89f - shootAmt);
cue.transform.localRotation = r * xr;
cue.transform.position = transform_Surface.TransformPoint(ball_CO[0] + (r * xr * worldHit));
}
dkCursorObj.transform.localPosition = dkCursor;
dkOverlayPwr.transform.localScale = new Vector3(1.0f - (shootAmt * 2.0f), 1.0f, 1.0f);
}
#region R_UNITY_MAGIC
void _hit_general()
{
// Make sure repositioner is turned off if the player decides he just
// wanted to hit it without putting it somewhere
isReposition = false;
markerObj.SetActive(false);
devhit.SetActive(false);
guideline.SetActive(false);
// Remove locks
_tr_endhit();
sn_permit = false;
sn_foul = false; // In case did not drop foul marker
#if HT8B_DEBUGGER
_log( LOG_LOW + "Commiting changes" + LOG_END );
#endif
// Commit changes
sn_simulating = true;
sn_pocketed_prv = sn_pocketed;
// Make sure we definately are the network owner
Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
_netpack(sn_turnid);
_netread();
sn_oursim = true;
balls_render[0].GetComponent<AudioSource>().PlayOneShot(snd_hitball, 1.0f);
}
private void Update()
{
// Physics step accumulator routine
float time = Time.timeSinceLevelLoad;
float timeDelta = time - timeLast;
timeLast = time;
if (particle_alive)
{
_vis_floaty_eval();
}
if (dk_shootui)
{
_ht_desktopui_update();
}
// Run sim only if things are moving
if (sn_simulating)
{
accum += timeDelta;
if (accum > k_MAX_DELTA)
{
accum = k_MAX_DELTA;
}
while (accum >= k_FIXED_TIME_STEP)
{
_phys_step();
accum -= k_FIXED_TIME_STEP;
}
}
else
{
// Control is in menu behaviour
if (sn_gameover)
{
_htmenu_update();
return;
}
}
// Update rendering objects positions
uint ball_bit = 0x1u;
for (int i = 0; i < 16; i++)
{
if ((ball_bit & sn_pocketed) == 0x0u)
{
balls_render[i].transform.localPosition = ball_CO[i];
}
ball_bit <<= 1;
}
cue_lpos = transform_Surface.InverseTransformPoint(cuetip.transform.position);
Vector3 lpos2 = cue_lpos;
// if shot is prepared for next hit
if (sn_permit)
{
bool isContact = false;
if (isReposition)
{
// Clamp position to table / kitchen
Vector3 temp = markerObj.transform.localPosition;
temp.x = Mathf.Clamp(temp.x, -k_pR.x, repoMaxX);
temp.z = Mathf.Clamp(temp.z, -k_pO.z, k_pO.z);
temp.y = 0.0f;
markerObj.transform.localPosition = temp;
markerObj.transform.localRotation = Quaternion.identity;
ball_CO[0] = temp;
balls_render[0].transform.localPosition = temp;
isContact = _cue_contacting();
if (isContact)
{
markerMaterial.SetColor(uniform_marker_colour, k_markerColourNO);
}
else
{
markerMaterial.SetColor(uniform_marker_colour, k_markerColourOK);
}
}
Vector3 cueball_pos = ball_CO[0];
if (sn_armed && !isContact)
{
float sweep_time_ball = Vector3.Dot(cueball_pos - cue_llpos, cue_vdir);
// Check for potential skips due to low frame rate
if (sweep_time_ball > 0.0f && sweep_time_ball < (cue_llpos - lpos2).magnitude)
{
lpos2 = cue_llpos + cue_vdir * sweep_time_ball;
}
// Hit condition is when cuetip is gone inside ball
if ((lpos2 - cueball_pos).sqrMagnitude < k_BALL_RSQR)
{
Vector3 horizontal_force = lpos2 - cue_llpos;
horizontal_force.y = 0.0f;
// Compute velocity delta
float vel = (horizontal_force.magnitude / Time.deltaTime) * 1.5f;
Vector3 r = (RaySphere_output - cueball_pos) * k_BALL_1OR;
// Clamp velocity input to 16 m/s ( very strong break speed )
ball_V[0] = cue_shotdir * Mathf.Min(vel, 16.0f) * Vector3.Dot(r, -cue_shotdir);
// Angular velocity: L=r(normalized)×p
Vector3 p = cue_vdir * vel;
ball_W[0] = Vector3.Cross(r, p) * -50.0f;
#if HT8B_DEBUGGER
_log( LOG_WARN + "Angular velocity: " + ball_W[ 0 ].ToString() + ". Velocity: " + ball_V[ 0 ].ToString() + LOG_END );
#endif
_hit_general();
}
}
else
{
cue_vdir = this.transform.InverseTransformVector(cuetip.transform.forward);//new Vector2( cuetip.transform.forward.z, -cuetip.transform.forward.x ).normalized;
// Get where the cue will strike the ball
if (_phy_ray_sphere(lpos2, cue_vdir, cueball_pos))
{
guideline.SetActive(true);
devhit.SetActive(true);
devhit.transform.localPosition = RaySphere_output;
cue_shotdir = cue_vdir;
cue_shotdir.y = 0.0f;
if (dk_shootui)
{
}
else
{
// Compute deflection in VR mode
Vector3 scuffdir = (cueball_pos - RaySphere_output);
scuffdir.y = 0.0f;
cue_shotdir += scuffdir.normalized * 0.17f;
}
cue_fdir = Mathf.Atan2(cue_shotdir.z, cue_shotdir.x);
// Update the prediction line direction
guideline.transform.localPosition = ball_CO[0];
guideline.transform.localEulerAngles = new Vector3(0.0f, -cue_fdir * Mathf.Rad2Deg, 0.0f);
}
else
{
devhit.SetActive(false);
guideline.SetActive(false);
}
}
}
cue_llpos = lpos2;
// Table outline colour
if (sn_gameover)
{
// Flashing if we won
#if !HT_QUEST
tableCurrentColour = tableSrcColour * (Mathf.Sin(Time.timeSinceLevelLoad * 3.0f) * 0.5f + 1.0f);
#endif
infBaseTransform.transform.localPosition = new Vector3(0.0f, Mathf.Sin(Time.timeSinceLevelLoad) * 0.1f, 0.0f);
infBaseTransform.transform.Rotate(Vector3.up, 90.0f * Time.deltaTime);
}
else
{
#if !HT_QUEST
tableCurrentColour = Color.Lerp(tableCurrentColour, tableSrcColour, Time.deltaTime * 3.0f);
#else
// Run uniform updates at a slower rate on android (/8)
ANDROID_UNIFORM_CLOCK ++;
if( ANDROID_UNIFORM_CLOCK >= ANDROID_CLOCK_DIVIDER )
{
tableCurrentColour = Color.Lerp( tableCurrentColour, tableSrcColour, Time.deltaTime * 24.0f );
tableMaterial.SetColor( uniform_tablecolour, tableCurrentColour );
ANDROID_UNIFORM_CLOCK = 0x00u;
}
#endif
}
float time_percentage;
if (timer_running)
{
float timeleft = timer_end - Time.timeSinceLevelLoad;
if (timeleft < 0.0f)
{
_onlocal_timer_end();
time_percentage = 0.0f;
}
else
{
time_percentage = 1.0f - (timeleft * timer_recip);
}
}
else
{
time_percentage = 0.0f;
}
#if !HT_QUEST
tableMaterial.SetColor(uniform_tablecolour, tableCurrentColour);
#endif
timerMaterial.SetFloat("_TimeFrac", time_percentage);
// Intro animation
if (introAminTimer > 0.0f)
{
introAminTimer -= Time.deltaTime;
Vector3 temp;
float atime;
float aitime;
if (introAminTimer < 0.0f)
introAminTimer = 0.0f;
// Cueball drops late
temp = balls_render[0].transform.localPosition;
atime = Mathf.Clamp(introAminTimer - 0.33f, 0.0f, 1.0f);
aitime = (1.0f - atime);
temp.y = Mathf.Abs(Mathf.Cos(atime * 6.29f)) * atime * 0.5f;
balls_render[0].transform.localPosition = temp;
balls_render[0].transform.localScale = new Vector3(aitime, aitime, aitime);
for (int i = 1; i < 16; i++)
{
temp = balls_render[i].transform.localPosition;
atime = Mathf.Clamp(introAminTimer - 0.84f - (float)i * 0.03f, 0.0f, 1.0f);
aitime = (1.0f - atime);
temp.y = Mathf.Abs(Mathf.Cos(atime * 6.29f)) * atime * 0.5f;
balls_render[i].transform.localPosition = temp;
balls_render[i].transform.localScale = new Vector3(aitime, aitime, aitime);
}
}
}
private void Start()
{
#if HT8B_DEBUGGER
_log( LOG_LOW + "(init) Assigning constants & building collision data" + LOG_END );
#endif
// static assignment for config vars
repoMaxX = k_TABLE_WIDTH;
dkClampX = k_TABLE_WIDTH;
dkClampY = k_TABLE_HEIGHT;
// Overrides for 4 balls use the last 4 of the 16. (shared 9/4 texture)
for (int i = 0; i < 4; i++)
{
meshes_balls_override[i] = balls_render[12 + i].GetComponent<MeshFilter>().sharedMesh;
}
meshes_balls_regular[0] = balls_render[0].GetComponent<MeshFilter>().sharedMesh;
for (int i = 0; i < 3; i++)
{
meshes_balls_regular[i + 1] = balls_render[13 + i].GetComponent<MeshFilter>().sharedMesh;
}
Transform table_base = this.transform.Find("intl.table").Find("table_artwork");
auto_pocketblockers = table_base.Find(".4BALL_FILL").gameObject;
auto_rackPosition = table_base.Find(".RACK").gameObject;
auto_colliderBaseVFX = table_base.Find("collision.vfx").gameObject;
transform_Surface = this.transform.Find("intl.balls");
_phy_table_init();
aud_main = this.GetComponent<AudioSource>();
_htmenu_init();
_sn_cpyprev();
// turn off guideline
_vis_disableobjects();
#if UNITY_EDITOR
_vis_updatecoloursources();
return;
#endif
#if HT8B_DEBUGGER
_log( LOG_LOW + "Starting" + LOG_END );
#endif
guidelineMat.SetMatrix("_BaseTransform", this.transform.worldToLocalMatrix);
reflection_main.RenderProbe();
}
#endregion
int[] break_order_8ball = { 9, 2, 10, 11, 1, 3, 4, 12, 5, 13, 14, 6, 15, 7, 8 };
int[] break_order_9ball = { 2, 3, 4, 5, 9, 6, 7, 8, 1 };
int[] break_rows_9ball = { 0, 1, 2, 1, 0 };
// Resets local game state to defined state
// TODO: Merge this with NewGame()
public void _setup_break()
{
#if HT8B_DEBUGGER
_log( LOG_LOW + "SetupBreak()" + LOG_END );
#endif
sn_simulating = false;
sn_open = true;
sn_gameover = false;
sn_playerxor = 0;
sn_winnerid = 0;
// Cue ball
ball_CO[0] = new Vector3(-k_SPOT_POSITION_X, 0.0f, 0.0f);
ball_V[0] = Vector3.zero;
// Start at spot
if (gm_is_1) // 9 ball
{
sn_pocketed = 0xFC00u;
for (int i = 0, k = 0; i < 5; i++)
{
int rown = break_rows_9ball[i];
for (int j = 0; j <= rown; j++)
{
ball_CO[break_order_9ball[k++]] = new Vector3
(
k_SPOT_POSITION_X + (float)i * k_BALL_PL_Y + UnityEngine.Random.Range(-k_RANDOMIZE_F, k_RANDOMIZE_F),
0.0f,
(float)(-rown + j * 2) * k_BALL_PL_X + UnityEngine.Random.Range(-k_RANDOMIZE_F, k_RANDOMIZE_F)
);
ball_V[k] = Vector3.zero;
ball_W[k] = Vector3.zero;
}
}
}
else if (gm_is_2) // 4 ball
{
sn_pocketed = 0xFDF2u;
ball_CO[0] = new Vector3(-k_SPOT_CAROM_X, 0.0f, 0.0f);
ball_CO[13] = new Vector3(k_SPOT_CAROM_X, 0.0f, 0.0f);
ball_CO[14] = new Vector3(k_SPOT_POSITION_X, 0.0f, 0.0f);
ball_CO[15] = new Vector3(-k_SPOT_POSITION_X, 0.0f, 0.0f);
ball_V[0] = Vector3.zero;
ball_V[13] = Vector3.zero;
ball_V[14] = Vector3.zero;
ball_V[15] = Vector3.zero;
ball_W[0] = Vector3.zero;
ball_W[13] = Vector3.zero;
ball_W[14] = Vector3.zero;
ball_W[15] = Vector3.zero;
}
else // Normal 8 ball modes
{
sn_pocketed = 0x00u;
for (int i = 0, k = 0; i < 5; i++)
{
for (int j = 0; j <= i; j++)
{
ball_CO[break_order_8ball[k++]] = new Vector3
(
k_SPOT_POSITION_X + (float)i * k_BALL_PL_Y + UnityEngine.Random.Range(-k_RANDOMIZE_F, k_RANDOMIZE_F),
0.0f,
(float)(-i + j * 2) * k_BALL_PL_X + UnityEngine.Random.Range(-k_RANDOMIZE_F, k_RANDOMIZE_F)
);
ball_V[k] = Vector3.zero;
ball_W[k] = Vector3.zero;
}
}
}
sn_pocketed_prv = sn_pocketed;
}
#region R_INTERFACING
// Purpose:
// Public methods which should are called from other behaviours
// Player select 4 ball mode Japanese
public void _tr_yotsudama()
{
fb_jp = true;
fb_kr = false;
region_selected = true;
select4b.SetActive(false);
_tr_newgame();
}
public void _tr_sagu()
{
fb_jp = false;
fb_kr = true;
region_selected = true;
select4b.SetActive(false);
_tr_newgame();
}
// Player stopped holding input trigger
public void _tr_endhit()
{
sn_armed = false;
#if !HT_QUEST
guidelineMat.SetColor("_Colour", k_aimColour_aim);
#endif
}
// Player is holding input trigger
public void _tr_starthit()
{
// Release hit if cuetip is inside of ball
if (Vector3.Distance(cuetip.transform.position, ball_CO[0]) < k_BALL_RADIUS)
{
// TODO: play a sound here to signify error
// TODO: practice mode, allow dragging ball about
_tr_endhit();
return;
}
// lock aim variables
bool isOurTurn = ((local_playerid >= 0) && (local_teamid == sn_turnid)) || gm_practice;
if (isOurTurn)
{
sn_armed = true;
#if !HT_QUEST
guidelineMat.SetColor("_Colour", k_aimColour_locked);
#endif
}
}
// Player was moving cueball, place it down
public void _tr_placeball()
{
if (!_cue_contacting())
{
isReposition = false;
markerObj.SetActive(false);
sn_permit = true;
sn_foul = false;
Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
// Save out position to remote clients
_netpack(sn_turnid);
_netread();
}
}
// Initialize new match as the host
public void _tr_newgame()
{
// Check if game in progress
if (sn_gameover)
{
#if HT8B_DEBUGGER
_log( LOG_YES + "Starting new game" + LOG_END );
#endif
// Get gamestate rolling
sn_gameid++;
sn_permit = true;
_onlocal_newgame();
sn_turnid = 0;
sn_turnid_prv = 0;
_onlocal_turnchange();
// Following is overrides of NewGameLocal, for game STARTER only
_setup_break();
_vis_apply_tablecolour(0);
Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
_netpack(0);
_netread();
// Override allow repositioning within kitchen
// Local effector
isReposition = true;
repoMaxX = -k_SPOT_POSITION_X;
markerObj.transform.localPosition = ball_CO[0];
markerObj.SetActive(true);
if (!region_selected)
{
if (sn_gamemode == 2u)
{
select4b.SetActive(true);
return;
}
}
}
else
{
// Should not be hit since v1.0.0
#if HT8B_DEBUGGER
_log( LOG_ERR + "game in progress" + LOG_END );
#endif
}
}
// Completely reset ht8b state
public void _tr_force_end()
{
// Limit reset to totem owners ownly, this will always be someone in the room
// but it may not be obvious to players who has the ownership. So a info text
// is added above the reset button telling them who can reset if they dont have it
// this is simply to prevent trolls running in and force resetting at random
if (Networking.LocalPlayer == Networking.GetOwner(playerTotems[0]) ||
Networking.LocalPlayer == Networking.GetOwner(playerTotems[1])
|| sn_gameover || _name_gen.istrust())
{
#if HT8B_DEBUGGER
_log( LOG_WARN + "Ending game early" + LOG_END );
#endif
sn_gameover = true;
sn_permit = false;
sn_simulating = false;
// For good measure in case other clients trigger an event whilst owner
sn_packetid += 2;
Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
_netpack(sn_turnid);
_netread();
_onlocal_gameover();
infReset.text = "Reset";
}
else
{
// TODO: Make this a panel
#if HT8B_DEBUGGER
_log( LOG_ERR + "Reset is availible to: " + Networking.GetOwner( playerTotems[0] ).displayName + " and " + Networking.GetOwner( playerTotems[1] ).displayName + LOG_END );
#endif
infReset.text = "Only:\n" + Networking.GetOwner(playerTotems[0]).displayName + " and " + Networking.GetOwner(playerTotems[1]).displayName + "\ncan reset";
}
}
#endregion
#region R_NETWORK
const float I16_MAXf = 32767.0f;
void _encode_u16(int pos, ushort v)
{
net_data[pos] = (byte)(v & 0xff);
net_data[pos + 1] = (byte)(((uint)v >> 8) & 0xff);
}
ushort _decode_u16(int pos)
{
return (ushort)(net_data[pos] | (((uint)net_data[pos + 1]) << 8));
}
// 4 char string from Vector2. Encodes floats in: [ -range, range ] to 0-65535
void _encode_vec3(int pos, Vector3 vec, float range)
{
_encode_u16(pos, (ushort)((vec.x / range) * I16_MAXf + I16_MAXf));
_encode_u16(pos + 2, (ushort)((vec.z / range) * I16_MAXf + I16_MAXf));
}
// 6 char string from Vector3. Encodes floats in: [ -range, range ] to 0-65535
void _encode_vec3_full(int pos, Vector3 vec, float range)
{
_encode_u16(pos, (ushort)((Mathf.Clamp(vec.x, -range, range) / range) * I16_MAXf + I16_MAXf));
_encode_u16(pos + 2, (ushort)((Mathf.Clamp(vec.y, -range, range) / range) * I16_MAXf + I16_MAXf));
_encode_u16(pos + 4, (ushort)((Mathf.Clamp(vec.z, -range, range) / range) * I16_MAXf + I16_MAXf));
}
// Decode 4 chars at index to Vector3 (x,z). Decodes from 0-65535 to [ -range, range ]
Vector3 _decode_vec3(int start, float range)
{
ushort _x = _decode_u16(start);
ushort _y = _decode_u16(start + 2);
float x = ((_x - I16_MAXf) / I16_MAXf) * range;
float y = ((_y - I16_MAXf) / I16_MAXf) * range;
return new Vector3(x, 0.0f, y);
}
// Decode 6 chars at index to Vector3. Decodes from 0-65535 to [ -range, range ]
Vector3 _decode_vec3_full(int start, float range)
{
ushort _x = _decode_u16(start);
ushort _y = _decode_u16(start + 2);
ushort _z = _decode_u16(start + 4);
float x = ((_x - I16_MAXf) / I16_MAXf) * range;
float y = ((_y - I16_MAXf) / I16_MAXf) * range;
float z = ((_z - I16_MAXf) / I16_MAXf) * range;
return new Vector3(x, y, z);
}
float _decode_f32(int addr, float range)
{
return ((_decode_u16(addr) - I16_MAXf) / I16_MAXf) * range;
}
Color _decode_colour(int addr)
{
ushort _r = _decode_u16(addr);
ushort _g = _decode_u16(addr + 2);
ushort _b = _decode_u16(addr + 4);
ushort _a = _decode_u16(addr + 6);
return new Color
(
((_r - I16_MAXf) / I16_MAXf) * 20.0f,
((_g - I16_MAXf) / I16_MAXf) * 20.0f,
((_b - I16_MAXf) / I16_MAXf) * 20.0f,
((_a - I16_MAXf) / I16_MAXf) * 20.0f
);
}
public void _netpack_lossy()
{
if (!sn_gameover)
{
#if HT8B_DEBUGGER
_log( LOG_ERR + "Critical error: gameover was false when trying to _netpack_lossy()" + LOG_END );
#endif
return;
}
// Game state
uint flags = 0x20u; // bit #
// Since v1.0.0
flags |= sn_gamemode << 8; // 8 - 3 bits
flags |= sn_timer << 13; // 13 - 2 bits
if (sn_teams) flags |= 0x8000u; // 15 - 1 bit
if (sn_lobbyclosed) flags |= 0x800u;
// 1.5.3: sn_winnerid was being discarded, keep value alive for winner text
flags |= sn_winnerid << 6; // 6
_encode_u16(0x4C, (ushort)flags);
sn_packetid = (ushort)(sn_packetid + 1u);
_encode_u16(0x4E, (ushort)(sn_packetid));
_encode_u16(0x50, sn_gameid);
netstr = Convert.ToBase64String(net_data, Base64FormattingOptions.None);
}
// Encode all data of game state into netstr
public void _netpack(uint _turnid)
{
if (local_playerid < 0)
{
#if HT8B_DEBUGGER
_log( LOG_ERR + "Critical error: local_playerid was less than 0 when trying to NetPack()" + LOG_END );
#endif
return;
}
// Garuntee array size by reallocating.. because c#
net_data = new byte[0x52];
for (int i = 0; i < 16; i++)
{
_encode_vec3(i * 4, ball_CO[i], 2.5f);
}
// Cue ball velocity & angular velocity last
_encode_vec3(0x40, ball_V[0], 50.0f);
_encode_vec3_full(0x44, ball_W[0], 500.0f);
if (gm_is_2)
{
// Encode player scores into gmspec
sn_gmspec = (ushort)(((uint)fb_scores[0]) & 0x0fu);
sn_gmspec |= (ushort)((((uint)fb_scores[1]) & 0x0fu) << 4);
if (fb_kr) sn_gmspec |= (ushort)0x100u;
// 4 ball specifc ( no pocket info )
_encode_u16(0x4A, sn_gmspec);
}
else
{
// Encode pocketed imformation
_encode_u16(0x4A, (ushort)(sn_pocketed & 0x0000FFFFu));
}
// Game state
uint flags = 0x0U; // bit #
if (sn_simulating) flags |= 0x1U; // 0
flags |= _turnid << 1; // 1
if (sn_foul) flags |= 0x4U; // 2
if (sn_open) flags |= 0x8U; // 3
flags |= sn_playerxor << 4; // 4
if (sn_gameover) flags |= 0x20u; // 5
flags |= sn_winnerid << 6; // 6
if (sn_permit) flags |= 0x80U; // 7
if (sn_lobbyclosed) flags |= 0x800u;
// Since v1.0.0
flags |= sn_gamemode << 8; // 8 - 3 bits
flags |= sn_timer << 13; // 13 - 2 bits
if (sn_teams) flags |= 0x8000u; // 15 - 1 bit
_encode_u16(0x4C, (ushort)flags);
// Player ID msb gets added to referee any discrepencies between clients
// Higher order players get priority because it will be less common
// to play 2v2, so we can save most packet id's for normal 1v1
uint msb_playerid = ((uint)local_playerid & 0x2u) >> 1;
_encode_u16(0x4E, (ushort)(sn_packetid + 1u + msb_playerid));
_encode_u16(0x50, sn_gameid);
netstr = Convert.ToBase64String(net_data, Base64FormattingOptions.None);
#if HT8B_DEBUGGER
_log( LOG_LOW + "NetPack()" + LOG_END );
#endif
}
// Decode networking string
// TODO: Clean up this function
public void _netread()
{
// CHECK ERROR ===================================================================================================
#if HT8B_DEBUGGER
_log( LOG_LOW + "incoming base64: " + netstr + LOG_END );
#endif
byte[] in_data = Convert.FromBase64String(netstr);
if (in_data.Length < 0x52)
{
#if HT8B_DEBUGGER
_log( LOG_WARN + "Sync string too short for decode, skipping\n" + LOG_END );
#endif
return;
}
net_data = in_data;
#if HT8B_DEBUGGER
_log( LOG_LOW + _netstr_hex() + LOG_END );
#endif
// Throw out updates that are possible errournous
ushort nextid = _decode_u16(0x4E);
// Reset packetid if a new game has started
if (nextid <= sn_packetid)
{
#if HT8B_DEBUGGER
_log( LOG_WARN + "Packet ID was old ( " + nextid + " <= " + sn_packetid + " )" + LOG_END );
#endif
return;
}
sn_packetid = nextid;
// MAIN DECODE ===================================================================================================
_sn_cpyprev();
// Pocketed information
// Ball positions, reset velocity
for (int i = 0; i < 16; i++)
{
ball_V[i] = Vector3.zero;
ball_W[i] = Vector3.zero;
ball_CO[i] = _decode_vec3(i * 4, 2.5f);
}
ball_V[0] = _decode_vec3(0x40, 50.0f);
ball_W[0] = _decode_vec3_full(0x44, 500.0f);
sn_pocketed = _decode_u16(0x4A);
uint gamestate = _decode_u16(0x4C);
sn_simulating = (gamestate & 0x1U) == 0x1U;
sn_turnid = (gamestate & 0x2U) >> 1;
sn_foul = (gamestate & 0x4U) == 0x4U;
sn_open = (gamestate & 0x8U) == 0x8U;
sn_playerxor = (gamestate & 0x10U) >> 4;
sn_gameover = (gamestate & 0x20U) == 0x20U;
sn_winnerid = (gamestate & 0x40U) >> 6;
sn_permit = (gamestate & 0x80U) == 0x80U;
sn_lobbyclosed = (gamestate & 0x800u) == 0x800u;
// Since v1.0.0
sn_gamemode = (gamestate & 0x700u) >> 8; // 3 bits
sn_timer = (gamestate & 0x6000u) >> 13; // 2 bits
sn_teams = (gamestate & 0x8000u) == 0x8000u; //
sn_gameid = _decode_u16(0x50);
// Events ==========================================================================================================
if (sn_gameid > sn_gameid_prv && !sn_gameover)
{
// EV: 1
#if HT8B_DEBUGGER
_log( LOG_YES + " .EV: 1 (sn_gameid > sn_gameid_prv) -> NewGame" + LOG_END );
#endif
_onlocal_newgame();
}
// Check if turn was transferred
if (sn_turnid != sn_turnid_prv)
{
// EV: 2
#if HT8B_DEBUGGER
_log( LOG_YES + " .EV: 2 (sn_turnid != sn_turnid_prv) -> NewTurn" + LOG_END );
#endif
_onlocal_turnchange();
}
// Table switches to closed
if (sn_open_prv && !sn_open)
{
// EV: 3
#if HT8B_DEBUGGER
_log( LOG_YES + " .EV: 3 (sn_open_prv && !sn_open) -> DisplaySet" + LOG_END );
#endif
_onlocal_tableclosed();
}
// Check if game is over
if (!sn_gameover_prv && sn_gameover)
{
// EV: 4
#if HT8B_DEBUGGER
_log( LOG_YES + " .EV: 4 (!sn_gameover_prv && sn_gamemover) -> Gameover" + LOG_END );
#endif
_onlocal_gameover();
return;
}
if (sn_gameover)
{
_htmenu_viewtimer();
_htmenu_viewteams();
_htmenu_viewgm();
_htmenu_viewjoin();
_htmenuview();
return;
}
// Effects colliders need to be turned off when not simulating
// to improve pickups being glitchy
if (sn_simulating)
{
auto_colliderBaseVFX.SetActive(true);
}
else
{
auto_colliderBaseVFX.SetActive(false);
}
if (gm_is_2)
{
sn_gmspec = _decode_u16(0x4A);
fb_scores[0] = (int)(sn_gmspec & 0x0fu);
fb_scores[1] = (int)((sn_gmspec & 0xf0u) >> 4);
fb_kr = (sn_gmspec & 0x100u) == 0x100u;
fb_jp = !fb_kr;
sn_pocketed = 0x1FFEu;
}
// Check this every read
// Its basically 'turn start' event
if (sn_permit)
{
bool isOurTurn = ((local_playerid >= 0) && (local_teamid == sn_turnid)) || gm_practice;
// Check if teammate placed the positioner
if (!sn_foul)
{
#if HT8B_DEBUGGER
_log( LOG_YES + " .EV: 3 (!sn_foul && sn_foul_prv && sn_permit) -> Marker placed" + LOG_END );
#endif
isReposition = false;
markerObj.SetActive(false);
}
#if !HT_QUEST
if (isOurTurn)
{
// Update for desktop
_dk_AllowHit();
}
else
{
_dk_DenyHit();
}
#endif
if (gm_is_1)
{
int target = _lowest_ball(sn_pocketed);
marker9ball.SetActive(true);
marker9ball.transform.localPosition = ball_CO[target];
}
_vis_rackballs();
if (sn_timer > 0 && !timer_running)
{
_timer_reset();
}
}
else
{
marker9ball.SetActive(false);
timer_running = false;
fb_madepoint = false;
fb_madefoul = false;
sn_firsthit = 0;
sn_secondhit = 0;
sn_thirdhit = 0;
_vis_hidetimers();
// These dissapeared from v1.0.0 for some reason
markerObj.SetActive(false);
devhit.SetActive(false);
guideline.SetActive(false);
}
_onlocal_updatescorecard();
}
string _netstr_hex()
{
string str = "";
for (int i = 0; i < net_data.Length; i += 2)
{
ushort v = _decode_u16(i);
str += v.ToString("X4");
}
return str;
}
// Wait for updates to the synced netstr
public override void OnDeserialization()
{
if (!string.Equals(netstr, netstr_prv))
{
#if HT8B_DEBUGGER
_log( LOG_LOW + "OnDeserialization() :: netstr update" + LOG_END );
#endif
netstr_prv = netstr;
// Check if local simulation is in progress, the event will fire off later when physics
// are settled by the client
if (sn_simulating)
{
#if HT8B_DEBUGGER
_log( LOG_WARN + "local simulation is still running, the network update will occur after completion" + LOG_END );
#endif
sn_updatelock = true;
}
else
{
// We are free to read this update
_netread();
}
}
}
#endregion
#if !HT_QUEST
const int LOG_MAX = 32;
int LOG_LEN = 0;
int LOG_PTR = 0;
string[] LOG_LINES = new string[32];
// Print a line to the debugger
void _log(string ln)
{
Debug.Log("[<color=\"#B5438F\">ht8b</color>] " + ln);
LOG_LINES[LOG_PTR++] = "[<color=\"#B5438F\">ht8b</color>] " + ln + "\n";
LOG_LEN++;
if (LOG_PTR >= LOG_MAX)
{
LOG_PTR = 0;
}
if (LOG_LEN > LOG_MAX)
{
LOG_LEN = LOG_MAX;
}
string output = "ht8b 1.6.2";
// Add information about game state:
output += Networking.IsOwner(Networking.LocalPlayer, this.gameObject) ?
"<color=\"#95a2b8\">net(</color> <color=\"#4287F5\">OWNER</color> <color=\"#95a2b8\">)</color> " :
"<color=\"#95a2b8\">net(</color> <color=\"#678AC2\">RECVR</color> <color=\"#95a2b8\">)</color> ";
output += sn_simulating ?
"<color=\"#95a2b8\">sim(</color> <color=\"#4287F5\">ACTIVE</color> <color=\"#95a2b8\">)</color> " :
"<color=\"#95a2b8\">sim(</color> <color=\"#678AC2\">PAUSED</color> <color=\"#95a2b8\">)</color> ";
VRCPlayerApi currentOwner = Networking.GetOwner(this.gameObject);
output += "<color=\"#95a2b8\">player(</color> <color=\"#4287F5\">" + (currentOwner != null ? currentOwner.displayName : "[null]") + ":" + sn_turnid + "</color> <color=\"#95a2b8\">)</color>";
output += "\n---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n";
// Update display
for (int i = 0; i < LOG_LEN; i++)
{
output += LOG_LINES[(LOG_MAX + LOG_PTR - LOG_LEN + i) % LOG_MAX];
}
ltext.text = output;
}
#endif
}